use std::collections::{BTreeMap, BTreeSet};
use bevy::app::{App, FixedUpdate, Plugin};
use bevy::ecs::entity::Entity;
use bevy::ecs::message::{Message, MessageReader, MessageWriter};
use bevy::ecs::resource::Resource;
use bevy::ecs::schedule::IntoScheduleConfigs;
use bevy::ecs::system::{Query, Res, ResMut, SystemParam};
use bevy::log::error;
use crate::core::{PendingActions, PersonIndex, SocietyClock, SocietySystems, require_core};
use crate::defs::{ActivityId, InteractionId, SocietyDefs, TriggerKind, TriggerSpec};
use crate::mind::Needs;
use crate::relationships::{TieLabel, TieLabelChanged};
use crate::reputation::NormViolated;
use crate::rumor::RumorBook;
use crate::script::{Bindings, ScriptEngine, ScriptError};
use crate::snapshot::SnapshotCell;
#[derive(Message, Debug, Clone, PartialEq, Eq)]
pub struct FireTrigger {
pub name: String,
pub actor: Option<Entity>,
pub target: Option<Entity>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum TriggerEvent {
Custom {
name: String,
actor: i64,
target: i64,
},
Deceased {
actor: i64,
},
Interaction {
interaction: InteractionId,
actor: i64,
target: i64,
accepted: bool,
},
Activity {
activity: ActivityId,
actor: i64,
target: i64,
},
Dialogue {
conversation: String,
node: String,
actor: i64,
target: i64,
},
}
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct TriggerQueue(pub(crate) Vec<TriggerEvent>);
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct TriggerState {
fired: BTreeSet<(usize, i64)>,
cooling: BTreeMap<(usize, i64), f64>,
}
impl TriggerState {
fn may_fire(&self, index: usize, actor: i64, spec: &TriggerSpec, now_days: f64) -> bool {
let key = (index, actor);
if spec.once && self.fired.contains(&key) {
return false;
}
if spec.cooldown_hours > 0.0
&& let Some(&last) = self.cooling.get(&key)
&& (now_days - last) * 24.0 < f64::from(spec.cooldown_hours)
{
return false;
}
true
}
#[cfg(feature = "serde")]
pub(crate) fn fired_set(&self) -> &BTreeSet<(usize, i64)> {
&self.fired
}
#[cfg(feature = "serde")]
pub(crate) fn cooling_map(&self) -> &BTreeMap<(usize, i64), f64> {
&self.cooling
}
#[cfg(feature = "serde")]
pub(crate) fn replace(
&mut self,
fired: BTreeSet<(usize, i64)>,
cooling: BTreeMap<(usize, i64), f64>,
) {
self.fired = fired;
self.cooling = cooling;
}
fn record(&mut self, index: usize, actor: i64, spec: &TriggerSpec, now_days: f64) {
let key = (index, actor);
if spec.once {
self.fired.insert(key);
}
if spec.cooldown_hours > 0.0 {
self.cooling.insert(key, now_days);
}
}
}
#[derive(Resource, Debug, Clone, Copy, Default)]
pub(crate) struct LastDays(pub(crate) Option<f64>);
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct NeedArmed(pub(crate) Vec<Vec<bool>>);
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct RumorReported(pub(crate) BTreeSet<(u16, u32)>);
pub struct RulesPlugin;
impl Plugin for RulesPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<TriggerQueue>()
.init_resource::<TriggerState>()
.init_resource::<LastDays>()
.init_resource::<NeedArmed>()
.init_resource::<RumorReported>()
.add_message::<FireTrigger>()
.add_systems(
FixedUpdate,
queue_fired_triggers.in_set(SocietySystems::Clock),
)
.add_systems(FixedUpdate, evaluate_triggers.in_set(SocietySystems::Rules));
}
fn finish(&self, app: &mut App) {
require_core(app, "RulesPlugin");
}
}
fn queue_fired_triggers(
mut reader: MessageReader<FireTrigger>,
index: Res<PersonIndex>,
mut queue: Option<ResMut<TriggerQueue>>,
) {
let Some(queue) = queue.as_mut() else {
return;
};
for msg in reader.read() {
queue.0.push(TriggerEvent::Custom {
name: msg.name.clone(),
actor: id_of(&index, msg.actor),
target: id_of(&index, msg.target),
});
}
}
pub(crate) fn id_of(index: &PersonIndex, entity: Option<Entity>) -> i64 {
let Some(entity) = entity else {
return -1;
};
index
.sorted
.iter()
.find(|(_, e)| *e == entity)
.map_or(-1, |(id, _)| i64::from(id.0))
}
fn thresholds(defs: &SocietyDefs, need: u16) -> Vec<f32> {
defs.triggers
.iter()
.filter_map(|def| match def.on {
TriggerKind::NeedBelow { need: id, below } if id.0 == need => Some(below),
_ => None,
})
.collect()
}
#[derive(SystemParam)]
struct TriggerBookkeeping<'w> {
queue: Option<ResMut<'w, TriggerQueue>>,
state: Option<ResMut<'w, TriggerState>>,
last_days: Option<ResMut<'w, LastDays>>,
armed: Option<ResMut<'w, NeedArmed>>,
reported: Option<ResMut<'w, RumorReported>>,
pending: Option<ResMut<'w, PendingActions>>,
}
#[allow(clippy::too_many_arguments)]
fn evaluate_triggers(
clock: Res<SocietyClock>,
defs: Res<SocietyDefs>,
engine: Option<Res<ScriptEngine>>,
cell: Res<SnapshotCell>,
index: Res<PersonIndex>,
books: Res<RumorBook>,
mut book: TriggerBookkeeping,
mut label_changes: MessageReader<TieLabelChanged>,
mut violations: MessageReader<NormViolated>,
needs: Query<&Needs>,
mut errors: MessageWriter<ScriptError>,
) {
let (Some(engine), Some(queue), Some(state), Some(pending)) = (
engine,
book.queue.as_mut(),
book.state.as_mut(),
book.pending.as_mut(),
) else {
return;
};
let Some(snapshot) = cell.0.clone() else {
return;
};
let now = clock.now_days;
let mut events: Vec<(EventKey, Bindings)> = Vec::new();
events.push((EventKey::Tick, Bindings::of_actor(None)));
if let Some(last) = book.last_days.as_mut() {
match last.0 {
None => last.0 = Some(now),
Some(previous) => {
if now > previous {
events.push((
EventKey::Hours {
from: previous,
to: now,
},
Bindings::of_actor(None),
));
}
last.0 = Some(now);
}
}
}
events.extend(queue.0.drain(..).map(|event| match event {
TriggerEvent::Deceased { actor } => (
EventKey::PersonDied,
Bindings {
actor,
..Bindings::default()
},
),
TriggerEvent::Custom {
name,
actor,
target,
} => (
EventKey::Custom(name),
Bindings {
actor,
target,
..Bindings::default()
},
),
TriggerEvent::Interaction {
interaction,
actor,
target,
accepted,
} => (
EventKey::Interaction(interaction),
Bindings {
actor,
target,
accepted: Some(accepted),
..Bindings::default()
},
),
TriggerEvent::Activity {
activity,
actor,
target,
} => (
EventKey::Activity(activity),
Bindings {
actor,
target,
..Bindings::default()
},
),
TriggerEvent::Dialogue {
conversation,
node,
actor,
target,
} => (
EventKey::Dialogue(conversation, node),
Bindings {
actor,
target,
..Bindings::default()
},
),
}));
for change in label_changes.read() {
events.push((
EventKey::Label(change.new),
Bindings {
actor: id_of(&index, Some(change.from)),
target: id_of(&index, Some(change.to)),
..Bindings::default()
},
));
}
for violation in violations.read() {
events.push((
EventKey::Violation(defs.deed_id(&violation.deed).map(|d| d.0)),
Bindings {
actor: id_of(&index, Some(violation.actor)),
target: id_of(&index, Some(violation.witness)),
..Bindings::default()
},
));
}
let need_count = defs.needs.len();
for n in 0..need_count {
let below = thresholds(&defs, n as u16);
if below.is_empty() {
continue;
}
let Some(armed) = book.armed.as_mut() else {
break;
};
if armed.0.len() < need_count {
armed.0.resize(need_count, Vec::new());
}
if armed.0[n].len() != below.len() {
armed.0[n] = vec![true; below.len()];
}
for &(_, entity) in &index.sorted {
let Ok(levels) = needs.get(entity) else {
continue;
};
let level = levels.level(n);
for (slot, &threshold) in below.iter().enumerate() {
let is_armed = armed.0[n][slot];
if level < threshold && is_armed {
armed.0[n][slot] = false;
events.push((
EventKey::Need(n as u16, threshold),
Bindings {
actor: id_of(&index, Some(entity)),
..Bindings::default()
},
));
} else if level > threshold + 0.05 {
armed.0[n][slot] = true;
}
}
}
}
if let Some(reported) = book.reported.as_mut() {
let living = index.sorted.len();
for (i, rumor) in books.rumors.iter().enumerate() {
if !rumor.alive || living == 0 {
continue;
}
let reach = rumor.known_by as f32 / living as f32;
let id = crate::rumor::RumorId(i as u32);
for def in &defs.triggers {
let TriggerKind::RumorReach { deed, fraction } = def.on else {
continue;
};
if deed != rumor.deed || reach < fraction {
continue;
}
if !reported.0.insert((deed.0, id.0)) {
continue;
}
events.push((
EventKey::RumorReach(deed.0),
Bindings {
subject: id_of(&index, Some(rumor.subject)),
rumor: i64::from(id.0),
..Bindings::default()
},
));
}
}
}
for (event, bindings) in events {
for (which, spec) in defs.triggers.iter().enumerate() {
if !event.matches(&spec.on) || !state.may_fire(which, bindings.actor, spec, now) {
continue;
}
if let Some(condition) = spec.condition
&& !condition_holds(&engine, &defs, condition, &snapshot, bindings, &mut errors)
{
continue;
}
match engine.run(&defs.scripts, spec.then, &snapshot, bindings) {
Ok(actions) => pending.0.extend(actions),
Err(err) => report(&defs, spec.then, err, &mut errors),
}
state.record(which, bindings.actor, spec, now);
}
}
}
fn condition_holds(
engine: &ScriptEngine,
defs: &SocietyDefs,
condition: crate::script::ScriptId,
snapshot: &std::sync::Arc<crate::snapshot::SocietySnapshot>,
bindings: Bindings,
errors: &mut MessageWriter<ScriptError>,
) -> bool {
match engine.eval_bool(&defs.scripts, condition, snapshot, bindings) {
Ok(holds) => holds,
Err(err) => {
report(defs, condition, err, errors);
false
}
}
}
pub(crate) fn report(
defs: &SocietyDefs,
which: crate::script::ScriptId,
err: String,
errors: &mut MessageWriter<ScriptError>,
) {
let owner = ScriptEngine::owner(&defs.scripts, which).to_owned();
error!("bevy_society: script {owner} failed: {err}");
errors.write(ScriptError {
owner,
message: err,
});
}
enum EventKey {
Tick,
Hours {
from: f64,
to: f64,
},
Custom(String),
PersonDied,
Interaction(InteractionId),
Activity(ActivityId),
Label(TieLabel),
Violation(Option<u16>),
Dialogue(String, String),
Need(u16, f32),
RumorReach(u16),
}
impl EventKey {
fn matches(&self, on: &TriggerKind) -> bool {
match (self, on) {
(Self::Tick, TriggerKind::Tick) => true,
(Self::Hours { from, to }, TriggerKind::Hour(want)) => {
let want = f64::from(*want);
((to * 24.0 - want) / 24.0).floor() > ((from * 24.0 - want) / 24.0).floor()
}
(Self::Custom(name), TriggerKind::Custom(want)) => name == want,
(Self::PersonDied, TriggerKind::PersonDied) => true,
(Self::Interaction(id), TriggerKind::Interaction(want)) => id == want,
(Self::Activity(id), TriggerKind::Activity(want)) => id == want,
(Self::Label(label), TriggerKind::TieLabel(want)) => label == want,
(Self::Violation(deed), TriggerKind::NormViolated(want)) => deed == &Some(want.0),
(
Self::Dialogue(conversation, node),
TriggerKind::DialogueNode {
conversation: want_conversation,
node: want_node,
},
) => conversation == want_conversation && node == want_node,
(Self::Need(n, threshold), TriggerKind::NeedBelow { need, below }) => {
*n == need.0 && (threshold - below).abs() < 1e-6
}
(Self::RumorReach(deed), TriggerKind::RumorReach { deed: want, .. }) => *deed == want.0,
_ => false,
}
}
}