From b2f92e4a6c9dcc6487074634f69ab3ddc27521c1 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Wed, 4 Jan 2023 11:25:39 +0000 Subject: [PATCH] Switch to combinator-driven NPC AI API --- rtsim/src/data/npc.rs | 441 ++++++++++++++++++------------- rtsim/src/gen/mod.rs | 4 +- rtsim/src/lib.rs | 4 +- rtsim/src/rule/npc_ai.rs | 120 ++++++++- server/agent/src/action_nodes.rs | 7 +- 5 files changed, 376 insertions(+), 200 deletions(-) diff --git a/rtsim/src/data/npc.rs b/rtsim/src/data/npc.rs index 929980adae..d0ee7b90a2 100644 --- a/rtsim/src/data/npc.rs +++ b/rtsim/src/data/npc.rs @@ -1,4 +1,4 @@ -use crate::rule::npc_ai; +use crate::rule::npc_ai::NpcCtx; pub use common::rtsim::{NpcId, Profession}; use common::{ comp, @@ -50,221 +50,295 @@ pub struct Controller { pub goto: Option<(Vec3, f32)>, } -#[derive(Default)] -pub struct TaskState { - state: Option>, +impl Controller { + pub fn idle() -> Self { Self { goto: None } } } -pub const CONTINUE: ControlFlow<()> = ControlFlow::Break(()); -pub const FINISH: ControlFlow<()> = ControlFlow::Continue(()); +pub trait Action: Any + Send + Sync { + /// Returns `true` if the action should be considered the 'same' (i.e: + /// achieving the same objective) as another. In general, the AI system + /// will try to avoid switching (and therefore restarting) tasks when the + /// new task is the 'same' as the old one. + // TODO: Figure out a way to compare actions based on their 'intention': i.e: + // two pathing actions should be considered equivalent if their destination + // is the same regardless of the progress they've each made. + fn is_same(&self, other: &Self) -> bool + where + Self: Sized; + fn dyn_is_same_sized(&self, other: &dyn Action) -> bool + where + Self: Sized, + { + match (other as &dyn Any).downcast_ref::() { + Some(other) => self.is_same(other), + None => false, + } + } + fn dyn_is_same(&self, other: &dyn Action) -> bool; + // Reset the action to its initial state so it can be restarted + fn reset(&mut self); -pub trait Task: PartialEq + Clone + Send + Sync + 'static { - type State: Send + Sync; - type Ctx<'a>; + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow; - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State; - - fn run<'a>( - &self, - state: &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()>; - - fn then(self, other: B) -> Then { Then(self, other) } - - fn repeat(self) -> Repeat { Repeat(self) } + fn then, R1>(self, other: A1) -> Then + where + Self: Sized, + { + Then { + a0: self, + a0_finished: false, + a1: other, + phantom: PhantomData, + } + } + fn repeat(self) -> Repeat + where + Self: Sized, + { + Repeat(self, PhantomData) + } + fn stop_if bool>(self, f: F) -> StopIf + where + Self: Sized, + { + StopIf(self, f) + } + fn map R1, R1>(self, f: F) -> Map + where + Self: Sized, + { + Map(self, f, PhantomData) + } } -#[derive(Clone, PartialEq)] -pub struct Then(A, B); +// Now -impl Task for Then -where - B: for<'a> Task = A::Ctx<'a>>, +#[derive(Copy, Clone)] +pub struct Now(F, Option); + +impl A + Send + Sync + 'static, A: Action> + Action for Now { - // TODO: Use `Either` instead - type Ctx<'a> = A::Ctx<'a>; - type State = Result; + // TODO: This doesn't compare?! + fn is_same(&self, other: &Self) -> bool { true } - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { Ok(self.0.begin(ctx)) } + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - fn run<'a>( - &self, - state: &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - match state { - Ok(a_state) => { - self.0.run(a_state, ctx, controller)?; - *state = Err(self.1.begin(ctx)); - CONTINUE - }, - Err(b_state) => self.1.run(b_state, ctx, controller), - } + fn reset(&mut self) { self.1 = None; } + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + (self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx) } } -#[derive(Clone, PartialEq)] -pub struct Repeat(A); - -impl Task for Repeat { - type Ctx<'a> = A::Ctx<'a>; - type State = A::State; - - fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { self.0.begin(ctx) } - - fn run<'a>( - &self, - state: &mut Self::State, - ctx: &Self::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - self.0.run(state, ctx, controller)?; - *state = self.0.begin(ctx); - CONTINUE - } +pub fn now(f: F) -> Now +where + F: FnMut(&mut NpcCtx) -> A, +{ + Now(f, None) } -impl TaskState { - pub fn perform<'a, T: Task>( - &mut self, - task: T, - ctx: &T::Ctx<'a>, - controller: &mut Controller, - ) -> ControlFlow<()> { - type StateOf = (T, ::State); +// Just - let mut state = if let Some(state) = self.state.take().and_then(|state| { - state - .downcast::>() - .ok() - .filter(|state| state.0 == task) - }) { - state - } else { - let mut state = task.begin(ctx); - Box::new((task, state)) - }; +#[derive(Copy, Clone)] +pub struct Just(F, PhantomData); - let res = state.0.run(&mut state.1, ctx, controller); +impl R + Send + Sync + 'static> Action + for Just +{ + fn is_same(&self, other: &Self) -> bool { true } - self.state = if matches!(res, ControlFlow::Break(())) { - Some(state) - } else { - None - }; + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - res - } + fn reset(&mut self) {} + + // TODO: Reset closure state? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { ControlFlow::Break((self.0)(ctx)) } } -pub unsafe trait Context { - // TODO: Somehow we need to enforce this bound, I think? - // Hence, this trait is unsafe for now. - type Ty<'a>; // where for<'a> Self::Ty<'a>: 'a; +pub fn just(mut f: F) -> Just +where + F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static, +{ + Just(f, PhantomData) } -pub struct Data(Arc>, PhantomData); - -impl Clone for Data { - fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) } -} - -impl Data { - pub fn with(&mut self, f: impl FnOnce(&mut C::Ty<'_>) -> R) -> R { - let ptr = self.0.swap(std::ptr::null_mut(), Ordering::Acquire); - if ptr.is_null() { - panic!("Data pointer was null, you probably tried to access data recursively") - } else { - // Safety: We have exclusive access to the pointer within this scope. - // TODO: Do we need a panic guard here? - let r = f(unsafe { &mut *(ptr as *mut C::Ty<'_>) }); - self.0.store(ptr, Ordering::Release); - r - } - } -} +// Tree pub type Priority = usize; -pub struct TaskBox { - task: Option<( - TypeId, - Box, Yield = A, Return = ()> + Unpin + Send + Sync>, - Priority, - )>, - data: Data, +const URGENT: Priority = 0; +const CASUAL: Priority = 1; + +pub struct Node(Box>, Priority); + +pub fn urgent, R>(a: A) -> Node { Node(Box::new(a), URGENT) } +pub fn casual, R>(a: A) -> Node { Node(Box::new(a), CASUAL) } + +pub struct Tree { + next: F, + prev: Option>, + interrupt: bool, } -impl TaskBox { - pub fn new(data: Data) -> Self { Self { task: None, data } } +impl Node + Send + Sync + 'static, R: 'static> Action + for Tree +{ + fn is_same(&self, other: &Self) -> bool { true } - #[must_use] - pub fn finish(&mut self, prio: Priority) -> ControlFlow { - if let Some((_, task, _)) = &mut self.task.as_mut().filter(|(_, _, p)| *p <= prio) { - match Pin::new(task).resume(self.data.clone()) { - GeneratorState::Yielded(action) => ControlFlow::Break(action), - GeneratorState::Complete(_) => { - self.task = None; - ControlFlow::Continue(()) - }, - } - } else { - ControlFlow::Continue(()) - } - } + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } - #[must_use] - pub fn perform, Yield = A, Return = ()> + Unpin + Any + Send + Sync>( - &mut self, - prio: Priority, - task: T, - ) -> ControlFlow { - let ty = TypeId::of::(); - if self - .task - .as_mut() - .filter(|(ty1, _, _)| *ty1 == ty) - .is_none() - { - self.task = Some((ty, Box::new(task), prio)); + fn reset(&mut self) { self.prev = None; } + + // TODO: Reset `next` too? + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + let new = (self.next)(ctx); + + let prev = match &mut self.prev { + Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => { + prev + }, + _ => self.prev.insert(new), }; - self.finish(prio) - } -} - -pub struct Brain { - task: Box, Yield = A, Return = !> + Unpin + Send + Sync>, - data: Data, -} - -impl Brain { - pub fn new, Yield = A, Return = !> + Unpin + Any + Send + Sync>( - task: T, - ) -> Self { - Self { - task: Box::new(task), - data: Data(Arc::new(AtomicPtr::new(std::ptr::null_mut())), PhantomData), - } - } - - pub fn tick(&mut self, ctx_ref: &mut C::Ty<'_>) -> A { - self.data - .0 - .store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst); - match Pin::new(&mut self.task).resume(self.data.clone()) { - GeneratorState::Yielded(action) => { - self.data.0.store(std::ptr::null_mut(), Ordering::Release); - action + match prev.0.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(r) => { + self.prev = None; + ControlFlow::Break(r) }, - GeneratorState::Complete(ret) => match ret {}, } } } +pub fn choose(f: F) -> impl Action +where + F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, +{ + Tree { + next: f, + prev: None, + interrupt: false, + } +} + +pub fn watch(f: F) -> impl Action +where + F: FnMut(&mut NpcCtx) -> Node + Send + Sync + 'static, +{ + Tree { + next: f, + prev: None, + interrupt: true, + } +} + +// Then + +#[derive(Copy, Clone)] +pub struct Then { + a0: A0, + a0_finished: bool, + a1: A1, + phantom: PhantomData, +} + +impl, A1: Action, R0: Send + Sync + 'static, R1: Send + Sync + 'static> + Action for Then +{ + fn is_same(&self, other: &Self) -> bool { + self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1) + } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { + self.a0.reset(); + self.a0_finished = false; + self.a1.reset(); + } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + if !self.a0_finished { + match self.a0.tick(ctx) { + ControlFlow::Continue(()) => return ControlFlow::Continue(()), + ControlFlow::Break(_) => self.a0_finished = true, + } + } + self.a1.tick(ctx) + } +} + +// Repeat + +#[derive(Copy, Clone)] +pub struct Repeat(A, PhantomData); + +impl> Action for Repeat { + fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + match self.0.tick(ctx) { + ControlFlow::Continue(()) => ControlFlow::Continue(()), + ControlFlow::Break(_) => { + self.0.reset(); + ControlFlow::Continue(()) + }, + } + } +} + +// StopIf + +#[derive(Copy, Clone)] +pub struct StopIf(A, F); + +impl, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action> + for StopIf +{ + fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } + + fn dyn_is_same(&self, other: &dyn Action>) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow> { + if (self.1)(ctx) { + ControlFlow::Break(None) + } else { + self.0.tick(ctx).map_break(Some) + } + } +} + +// Map + +#[derive(Copy, Clone)] +pub struct Map(A, F, PhantomData); + +impl, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1> + Action for Map +{ + fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) } + + fn dyn_is_same(&self, other: &dyn Action) -> bool { self.dyn_is_same_sized(other) } + + fn reset(&mut self) { self.0.reset(); } + + fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow { + self.0.tick(ctx).map_break(&mut self.1) + } +} + +pub struct Brain { + pub(crate) action: Box>, +} + #[derive(Serialize, Deserialize)] pub struct Npc { // Persisted state @@ -292,10 +366,7 @@ pub struct Npc { pub mode: NpcMode, #[serde(skip_serializing, skip_deserializing)] - pub task_state: Option, - - #[serde(skip_serializing, skip_deserializing)] - pub brain: Option>>, + pub brain: Option, } impl Clone for Npc { @@ -310,7 +381,6 @@ impl Clone for Npc { current_site: Default::default(), goto: Default::default(), mode: Default::default(), - task_state: Default::default(), brain: Default::default(), } } @@ -330,8 +400,7 @@ impl Npc { current_site: None, goto: None, mode: NpcMode::Simulated, - task_state: Default::default(), - brain: Some(npc_ai::brain()), + brain: None, } } diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index 7ff58bd0b6..49f34c8c0f 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -72,12 +72,12 @@ impl Data { .map(|e| e as f32 + 0.5) .with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0)) }; - for _ in 0..1 { + for _ in 0..10 { this.npcs.create( Npc::new(rng.gen(), rand_wpos(&mut rng)) .with_faction(site.faction) .with_home(site_id) - .with_profession(match 1/*rng.gen_range(0..20)*/ { + .with_profession(match rng.gen_range(0..20) { 0 => Profession::Hunter, 1 => Profession::Blacksmith, 2 => Profession::Chef, diff --git a/rtsim/src/lib.rs b/rtsim/src/lib.rs index db14aab7b5..dcf24c9e76 100644 --- a/rtsim/src/lib.rs +++ b/rtsim/src/lib.rs @@ -4,7 +4,9 @@ try_blocks, generator_trait, generators, - trait_alias + trait_alias, + trait_upcasting, + control_flow_enum )] pub mod data; diff --git a/rtsim/src/rule/npc_ai.rs b/rtsim/src/rule/npc_ai.rs index 0702b1165c..798670a5fe 100644 --- a/rtsim/src/rule/npc_ai.rs +++ b/rtsim/src/rule/npc_ai.rs @@ -3,8 +3,8 @@ use std::{collections::VecDeque, hash::BuildHasherDefault}; use crate::{ data::{ npc::{ - Brain, Context, Controller, Data, Npc, NpcId, PathData, PathingMemory, Task, TaskBox, - TaskState, CONTINUE, FINISH, + casual, choose, just, now, urgent, Action, Brain, Controller, Npc, NpcId, PathData, + PathingMemory, }, Sites, }, @@ -240,21 +240,27 @@ impl Rule for NpcAi { let npc_ids = ctx.state.data().npcs.keys().collect::>(); for npc_id in npc_ids { - let mut task_state = ctx.state.data_mut().npcs[npc_id] - .task_state - .take() - .unwrap_or_default(); let mut brain = ctx.state.data_mut().npcs[npc_id] .brain .take() - .unwrap_or_else(brain); + .unwrap_or_else(|| Brain { + action: Box::new(think().repeat()), + }); - let (controller, task_state) = { + let controller = { let data = &*ctx.state.data(); let npc = &data.npcs[npc_id]; let mut controller = Controller { goto: npc.goto }; + brain.action.tick(&mut NpcCtx { + ctx: &ctx, + npc, + npc_id, + controller: &mut controller, + }); + + /* let action: ControlFlow<()> = try { if matches!(npc.profession, Some(Profession::Adventurer(_))) { if let Some(home) = npc.home { @@ -344,12 +350,12 @@ impl Rule for NpcAi { */ } }; + */ - (controller, task_state) + controller }; ctx.state.data_mut().npcs[npc_id].goto = controller.goto; - ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state); ctx.state.data_mut().npcs[npc_id].brain = Some(brain); } }); @@ -358,6 +364,99 @@ impl Rule for NpcAi { } } +pub struct NpcCtx<'a> { + ctx: &'a EventCtx<'a, NpcAi, OnTick>, + npc_id: NpcId, + npc: &'a Npc, + controller: &'a mut Controller, +} + +fn idle() -> impl Action + Clone { just(|ctx| *ctx.controller = Controller::idle()) } + +fn move_toward(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { + const STEP_DIST: f32 = 10.0; + just(move |ctx| { + let rpos = wpos - ctx.npc.wpos; + let len = rpos.magnitude(); + ctx.controller.goto = Some(( + ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST), + speed_factor, + )); + }) +} + +fn goto(wpos: Vec3, speed_factor: f32) -> impl Action + Clone { + const MIN_DIST: f32 = 1.0; + + move_toward(wpos, speed_factor) + .repeat() + .stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < MIN_DIST.powi(2)) + .map(|_| {}) +} + +// Seconds +fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync { + let end = ctx.ctx.event.time.0 + time; + move |ctx| ctx.ctx.event.time.0 > end +} + +fn think() -> impl Action { + choose(|ctx| { + if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) { + // Choose a random site that's fairly close by + let site_wpos2d = ctx + .ctx + .state + .data() + .sites + .iter() + .filter(|(site_id, site)| { + site.faction.is_some() + && ctx.npc.current_site.map_or(true, |cs| *site_id != cs) + && thread_rng().gen_bool(0.25) + }) + .min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32) + .map(|(site_id, _)| site_id) + .and_then(|tgt_site| { + ctx.ctx + .state + .data() + .sites + .get(tgt_site) + .map(|site| site.wpos) + }); + + if let Some(site_wpos2d) = site_wpos2d { + // Walk toward the site + casual(goto( + site_wpos2d.map(|e| e as f32 + 0.5).with_z( + ctx.ctx + .world + .sim() + .get_alt_approx(site_wpos2d.as_()) + .unwrap_or(0.0), + ), + 1.0, + )) + } else { + casual(idle()) + } + } else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) { + casual(idle()) + } else { + casual( + now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0)) + .then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0))) + .repeat() + .stop_if(timeout(ctx, 10.0)) + .then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0)))) + .map(|_| {}), + ) + } + }) +} + +/* #[derive(Clone)] pub struct Generate(F, PhantomData); @@ -763,3 +862,4 @@ fn walk_path(site: Id, path: Path>) -> impl IsTask { println!("Waited."); } } +*/ diff --git a/server/agent/src/action_nodes.rs b/server/agent/src/action_nodes.rs index 2509c2ac3e..e4907eb356 100644 --- a/server/agent/src/action_nodes.rs +++ b/server/agent/src/action_nodes.rs @@ -229,11 +229,16 @@ impl<'a> AgentData<'a> { controller.push_cancel_input(InputKind::Fly) } + let chase_tgt = *travel_to/*read_data.terrain + .try_find_space(travel_to.as_()) + .map(|pos| pos.as_()) + .unwrap_or(*travel_to)*/; + if let Some((bearing, speed)) = agent.chaser.chase( &*read_data.terrain, self.pos.0, self.vel.0, - *travel_to, + chase_tgt, TraversalConfig { min_tgt_dist: 1.25, ..self.traversal_config