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::ecs::world::World;
use bevy::log::error;
use bevy::transform::components::Transform;
use crate::core::{
PendingActions, PersonId, PersonIndex, SocietyClock, SocietySettings, SocietySystems,
require_core,
};
use crate::defs::{
ActivityId, DefsError, DefsFile, 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(Message, Debug, Clone, PartialEq, Eq)]
pub struct TriggerFired {
pub trigger: String,
pub actor: Option<PersonId>,
pub target: Option<PersonId>,
}
#[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 NeedDisarmed(pub(crate) BTreeSet<(u16, u32, u32)>);
#[derive(Resource, Debug, Clone, Default)]
pub(crate) struct RumorReported(pub(crate) BTreeSet<(u16, u32)>);
pub(crate) struct TriggerRemap(Vec<Option<usize>>);
impl TriggerRemap {
pub(crate) fn new(old_ids: &[String], new_ids: &[String]) -> Self {
Self(
old_ids
.iter()
.map(|id| new_ids.iter().position(|n| n == id))
.collect(),
)
}
pub(crate) fn get(&self, old: usize) -> Option<usize> {
self.0.get(old).copied().flatten()
}
}
pub fn reload_defs(world: &mut World, file: DefsFile) -> Result<(), DefsError> {
let Some(mut defs) = world.get_resource::<SocietyDefs>().cloned() else {
return Err(DefsError::Invalid {
owner: "reload".to_owned(),
message: "no SocietyDefs resource to reload".to_owned(),
});
};
let old_ids: Vec<String> = defs.triggers.iter().map(|t| t.id.clone()).collect();
defs.reload(file)?;
let new_ids: Vec<String> = defs.triggers.iter().map(|t| t.id.clone()).collect();
let remap = TriggerRemap::new(&old_ids, &new_ids);
if let Some(mut state) = world.get_resource_mut::<TriggerState>() {
state.fired = std::mem::take(&mut state.fired)
.into_iter()
.filter_map(|(i, actor)| remap.get(i).map(|n| (n, actor)))
.collect();
state.cooling = std::mem::take(&mut state.cooling)
.into_iter()
.filter_map(|((i, actor), day)| remap.get(i).map(|n| ((n, actor), day)))
.collect();
}
world.insert_resource(defs);
Ok(())
}
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::<NeedDisarmed>()
.init_resource::<RumorReported>()
.add_message::<FireTrigger>()
.add_message::<TriggerFired>()
.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> {
let mut lines: Vec<f32> = Vec::new();
for def in &defs.triggers {
if let TriggerKind::NeedBelow { need: id, below } = def.on
&& id.0 == need
&& !lines.iter().any(|t| t.to_bits() == below.to_bits())
{
lines.push(below);
}
}
lines
}
#[derive(SystemParam)]
struct TriggerBookkeeping<'w> {
queue: Option<ResMut<'w, TriggerQueue>>,
state: Option<ResMut<'w, TriggerState>>,
last_days: Option<ResMut<'w, LastDays>>,
disarmed: Option<ResMut<'w, NeedDisarmed>>,
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>,
settings: Res<SocietySettings>,
places: Query<&Transform>,
memberships: Query<&crate::groups::Memberships>,
mut errors: MessageWriter<ScriptError>,
mut fired: MessageWriter<TriggerFired>,
) {
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 lines: Vec<(usize, Vec<f32>)> = (0..defs.needs.len())
.map(|n| (n, thresholds(&defs, n as u16)))
.filter(|(_, below)| !below.is_empty())
.collect();
if let Some(disarmed) = book.disarmed.as_mut() {
disarmed.0.retain(|&(need, bits, person)| {
index
.sorted
.binary_search_by_key(&person, |(id, _)| id.0)
.is_ok()
&& lines.iter().any(|(n, below)| {
*n == usize::from(need) && below.iter().any(|t| t.to_bits() == bits)
})
});
for (n, below) in &lines {
for &(id, entity) in &index.sorted {
let Ok(levels) = needs.get(entity) else {
continue;
};
let level = levels.level(*n);
for &threshold in below {
let key = (*n as u16, threshold.to_bits(), id.0);
if level < threshold {
if disarmed.0.insert(key) {
events.push((
EventKey::Need(*n as u16, threshold),
Bindings {
actor: i64::from(id.0),
..Bindings::default()
},
));
}
} else if level > threshold + 0.05 {
disarmed.0.remove(&key);
}
}
}
}
}
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 (!spec.factions.is_empty() || !spec.persons.is_empty())
&& !actor_selected(&index, &memberships, bindings.actor, spec)
{
continue;
}
if let Some(area) = spec.area
&& !actor_in_area(&index, &places, &settings, bindings.actor, area)
{
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);
fired.write(TriggerFired {
trigger: spec.id.clone(),
actor: person_id(bindings.actor),
target: person_id(bindings.target),
});
}
Err(err) => report(&defs, spec.then, err, &mut errors),
}
state.record(which, bindings.actor, spec, now);
}
}
}
fn person_id(bound: i64) -> Option<PersonId> {
u32::try_from(bound).ok().map(PersonId)
}
fn actor_selected(
index: &PersonIndex,
memberships: &Query<&crate::groups::Memberships>,
actor: i64,
spec: &crate::defs::TriggerSpec,
) -> bool {
let Some(&(id, entity)) = index.sorted.iter().find(|(id, _)| i64::from(id.0) == actor) else {
return false;
};
spec.persons.contains(&id.0)
|| memberships
.get(entity)
.is_ok_and(|m| spec.factions.iter().any(|f| m.contains(*f)))
}
fn actor_in_area(
index: &PersonIndex,
transforms: &Query<&Transform>,
settings: &SocietySettings,
actor: i64,
area: crate::defs::TriggerArea,
) -> bool {
let Some(&(_, entity)) = index.sorted.iter().find(|(id, _)| i64::from(id.0) == actor) else {
return false;
};
let Ok(transform) = transforms.get(entity) else {
return false;
};
let p = settings.plane.project(transform.translation);
area.contains([p.x, p.y])
}
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.to_bits() == below.to_bits()
}
(Self::RumorReach(deed), TriggerKind::RumorReach { deed: want, .. }) => *deed == want.0,
_ => false,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TriggerTest {
pub selected: bool,
pub in_area: bool,
pub condition: Option<bool>,
pub would_fire: bool,
pub effects: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TriggerTestError {
NoSociety,
NoSnapshot,
UnknownTrigger(String),
Script {
owner: String,
message: String,
},
}
impl std::fmt::Display for TriggerTestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoSociety => write!(f, "no SocietyDefs or ScriptEngine: add SocietyPlugins"),
Self::NoSnapshot => write!(
f,
"no snapshot yet: run a society tick (or fast_forward) before testing a trigger"
),
Self::UnknownTrigger(id) => write!(f, "no trigger with id {id:?}"),
Self::Script { owner, message } => write!(f, "script {owner} failed: {message}"),
}
}
}
impl std::error::Error for TriggerTestError {}
pub fn test_trigger(
world: &mut World,
trigger: &str,
actor: Option<PersonId>,
target: Option<PersonId>,
) -> Result<TriggerTest, TriggerTestError> {
let (Some(defs), Some(engine)) = (
world.get_resource::<SocietyDefs>(),
world.get_resource::<ScriptEngine>(),
) else {
return Err(TriggerTestError::NoSociety);
};
let snapshot = world
.get_resource::<SnapshotCell>()
.and_then(|cell| cell.0.clone())
.ok_or(TriggerTestError::NoSnapshot)?;
let spec = defs
.triggers
.iter()
.find(|t| t.id == trigger)
.ok_or_else(|| TriggerTestError::UnknownTrigger(trigger.to_owned()))?;
let bindings = Bindings {
actor: actor.map_or(-1, |p| i64::from(p.0)),
target: target.map_or(-1, |p| i64::from(p.0)),
..Bindings::default()
};
let failed = |script, message| TriggerTestError::Script {
owner: ScriptEngine::owner(&defs.scripts, script).to_owned(),
message,
};
let entity_of = |who: Option<PersonId>| {
let id = who?;
world
.get_resource::<PersonIndex>()?
.sorted
.iter()
.find(|(p, _)| *p == id)
.map(|(_, e)| *e)
};
let actor_entity = entity_of(actor);
let selected = (spec.factions.is_empty() && spec.persons.is_empty())
|| actor_entity.is_some_and(|e| {
actor.is_some_and(|p| spec.persons.contains(&p.0))
|| world
.get::<crate::groups::Memberships>(e)
.is_some_and(|m| spec.factions.iter().any(|f| m.contains(*f)))
});
let in_area = spec.area.is_none_or(|area| {
let (Some(e), Some(settings)) = (actor_entity, world.get_resource::<SocietySettings>())
else {
return false;
};
world.get::<Transform>(e).is_some_and(|t| {
let p = settings.plane.project(t.translation);
area.contains([p.x, p.y])
})
});
let mut result = TriggerTest {
selected,
in_area,
condition: None,
would_fire: false,
effects: Vec::new(),
};
if !selected || !in_area {
return Ok(result);
}
if let Some(condition) = spec.condition {
let holds = engine
.eval_bool(&defs.scripts, condition, &snapshot, bindings)
.map_err(|e| failed(condition, e))?;
result.condition = Some(holds);
if !holds {
return Ok(result);
}
}
let actions = engine
.run(&defs.scripts, spec.then, &snapshot, bindings)
.map_err(|e| failed(spec.then, e))?;
result.would_fire = true;
result.effects = actions.iter().map(|a| format!("{a:?}")).collect();
Ok(result)
}