mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Attempted generator-driven AI
This commit is contained in:
parent
0b4d3c9e20
commit
558b5f7c3a
@ -10,12 +10,16 @@ use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use slotmap::HopSlotMap;
|
||||
use std::{
|
||||
any::Any,
|
||||
any::{Any, TypeId},
|
||||
collections::VecDeque,
|
||||
ops::{ControlFlow, Deref, DerefMut},
|
||||
ops::{ControlFlow, Deref, DerefMut, Generator, GeneratorState},
|
||||
sync::{Arc, atomic::{AtomicPtr, Ordering}},
|
||||
pin::Pin,
|
||||
marker::PhantomData,
|
||||
};
|
||||
use vek::*;
|
||||
use world::{civ::Track, site::Site as WorldSite, util::RandomPerm};
|
||||
use crate::rule::npc_ai;
|
||||
|
||||
#[derive(Copy, Clone, Default)]
|
||||
pub enum NpcMode {
|
||||
@ -153,6 +157,110 @@ impl TaskState {
|
||||
}
|
||||
}
|
||||
|
||||
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 struct Data<C: Context>(Arc<AtomicPtr<()>>, PhantomData<C>);
|
||||
|
||||
impl<C: Context> Clone for Data<C> {
|
||||
fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) }
|
||||
}
|
||||
|
||||
impl<C: Context> Data<C> {
|
||||
pub fn with<R>(&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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type Priority = usize;
|
||||
|
||||
pub struct TaskBox<C: Context, A = ()> {
|
||||
task: Option<(
|
||||
TypeId,
|
||||
Box<dyn Generator<Data<C>, Yield = A, Return = ()> + Unpin + Send + Sync>,
|
||||
Priority,
|
||||
)>,
|
||||
data: Data<C>,
|
||||
}
|
||||
|
||||
impl<C: Context, A> TaskBox<C, A> {
|
||||
pub fn new(data: Data<C>) -> Self {
|
||||
Self {
|
||||
task: None,
|
||||
data,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn finish(&mut self, prio: Priority) -> ControlFlow<A> {
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn perform<T: Generator<Data<C>, Yield = A, Return = ()> + Unpin + Any + Send + Sync>(
|
||||
&mut self,
|
||||
prio: Priority,
|
||||
task: T,
|
||||
) -> ControlFlow<A> {
|
||||
let ty = TypeId::of::<T>();
|
||||
if self.task.as_mut().filter(|(ty1, _, _)| *ty1 == ty).is_none() {
|
||||
self.task = Some((ty, Box::new(task), prio));
|
||||
};
|
||||
|
||||
self.finish(prio)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Brain<C: Context, A = ()> {
|
||||
task: Box<dyn Generator<Data<C>, Yield = A, Return = !> + Unpin + Send + Sync>,
|
||||
data: Data<C>,
|
||||
}
|
||||
|
||||
impl<C: Context, A> Brain<C, A> {
|
||||
pub fn new<T: Generator<Data<C>, 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
|
||||
},
|
||||
GeneratorState::Complete(ret) => match ret {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Npc {
|
||||
// Persisted state
|
||||
@ -181,6 +289,9 @@ pub struct Npc {
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub task_state: Option<TaskState>,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub brain: Option<Brain<npc_ai::NpcData<'static>>>,
|
||||
}
|
||||
|
||||
impl Clone for Npc {
|
||||
@ -196,6 +307,7 @@ impl Clone for Npc {
|
||||
goto: Default::default(),
|
||||
mode: Default::default(),
|
||||
task_state: Default::default(),
|
||||
brain: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -215,6 +327,7 @@ impl Npc {
|
||||
goto: None,
|
||||
mode: NpcMode::Simulated,
|
||||
task_state: Default::default(),
|
||||
brain: Some(npc_ai::brain()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,19 +65,19 @@ impl Data {
|
||||
);
|
||||
|
||||
// Spawn some test entities at the sites
|
||||
for (site_id, site) in this.sites.iter() {
|
||||
for (site_id, site) in this.sites.iter().take(1) {
|
||||
let rand_wpos = |rng: &mut SmallRng| {
|
||||
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
|
||||
wpos2d
|
||||
.map(|e| e as f32 + 0.5)
|
||||
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
|
||||
};
|
||||
for _ in 0..20 {
|
||||
for _ in 0..1 {
|
||||
this.npcs.create(
|
||||
Npc::new(rng.gen(), rand_wpos(&mut rng))
|
||||
.with_faction(site.faction)
|
||||
.with_home(site_id)
|
||||
.with_profession(match rng.gen_range(0..20) {
|
||||
.with_profession(match 1/*rng.gen_range(0..20)*/ {
|
||||
0 => Profession::Hunter,
|
||||
1 => Profession::Blacksmith,
|
||||
2 => Profession::Chef,
|
||||
|
@ -1,8 +1,10 @@
|
||||
#![feature(
|
||||
explicit_generic_args_with_impl_trait,
|
||||
generic_associated_types,
|
||||
never_type,
|
||||
try_blocks
|
||||
try_blocks,
|
||||
generator_trait,
|
||||
generators,
|
||||
trait_alias
|
||||
)]
|
||||
|
||||
pub mod data;
|
||||
|
@ -2,7 +2,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault};
|
||||
|
||||
use crate::{
|
||||
data::{
|
||||
npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH},
|
||||
npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH, TaskBox, Brain, Data, Context},
|
||||
Sites,
|
||||
},
|
||||
event::OnTick,
|
||||
@ -241,6 +241,10 @@ impl Rule for NpcAi {
|
||||
.task_state
|
||||
.take()
|
||||
.unwrap_or_default();
|
||||
let mut brain = ctx.state.data_mut().npcs[npc_id]
|
||||
.brain
|
||||
.take()
|
||||
.unwrap_or_else(brain);
|
||||
|
||||
let (controller, task_state) = {
|
||||
let data = &*ctx.state.data();
|
||||
@ -295,6 +299,13 @@ impl Rule for NpcAi {
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
brain.tick(&mut NpcData {
|
||||
ctx: &ctx,
|
||||
npc,
|
||||
npc_id,
|
||||
controller: &mut controller,
|
||||
});
|
||||
/*
|
||||
// // Choose a random plaza in the npcs home site (which should be the
|
||||
// // current here) to go to.
|
||||
let task =
|
||||
@ -327,6 +338,7 @@ impl Rule for NpcAi {
|
||||
.repeat();
|
||||
|
||||
task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?;
|
||||
*/
|
||||
}
|
||||
};
|
||||
|
||||
@ -335,6 +347,7 @@ impl Rule for NpcAi {
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -599,3 +612,133 @@ impl Task for TravelTo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
let data = ctx.state.data();
|
||||
let site2 =
|
||||
npc.home.and_then(|home| data.sites.get(home)).and_then(
|
||||
|home| match &ctx.index.sites.get(home.world_site?).kind
|
||||
{
|
||||
SiteKind::Refactor(site2)
|
||||
| SiteKind::CliffTown(site2)
|
||||
| SiteKind::DesertCity(site2) => Some(site2),
|
||||
_ => None,
|
||||
},
|
||||
);
|
||||
|
||||
let wpos = site2
|
||||
.and_then(|site2| {
|
||||
let plaza = &site2.plots
|
||||
[site2.plazas().choose(&mut thread_rng())?];
|
||||
Some(site2.tile_center_wpos(plaza.root_tile()).as_())
|
||||
})
|
||||
.unwrap_or(npc.wpos.xy());
|
||||
|
||||
TravelTo {
|
||||
wpos,
|
||||
use_paths: true,
|
||||
}
|
||||
*/
|
||||
|
||||
trait IsTask = core::ops::Generator<Data<NpcData<'static>>, Yield = (), Return = ()> + Any + Send + Sync;
|
||||
|
||||
pub struct NpcData<'a> {
|
||||
ctx: &'a EventCtx<'a, NpcAi, OnTick>,
|
||||
npc_id: NpcId,
|
||||
npc: &'a Npc,
|
||||
controller: &'a mut Controller,
|
||||
}
|
||||
|
||||
unsafe impl Context for NpcData<'static> {
|
||||
type Ty<'a> = NpcData<'a>;
|
||||
}
|
||||
|
||||
pub fn brain() -> Brain<NpcData<'static>> {
|
||||
Brain::new(|mut data: Data<NpcData>| {
|
||||
let mut task = TaskBox::<_, ()>::new(data.clone());
|
||||
|
||||
loop {
|
||||
println!("Started");
|
||||
while let ControlFlow::Break(end) = task.finish(0) {
|
||||
yield end;
|
||||
}
|
||||
|
||||
// Choose a new plaza in the NPC's home site to path towards
|
||||
let path = data.with(|d| {
|
||||
let data = d.ctx.state.data();
|
||||
|
||||
let current_site = data.sites.get(d.npc.current_site?)?;
|
||||
let site2 = match &d.ctx.index.sites.get(current_site.world_site?).kind {
|
||||
SiteKind::Refactor(site2)
|
||||
| SiteKind::CliffTown(site2)
|
||||
| SiteKind::DesertCity(site2) => Some(site2),
|
||||
_ => None,
|
||||
}?;
|
||||
|
||||
let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?];
|
||||
let end_wpos = site2.tile_center_wpos(plaza.root_tile());
|
||||
|
||||
if end_wpos.as_::<f32>().distance(d.npc.wpos.xy()) < 32.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start = site2.wpos_tile_pos(d.npc.wpos.xy().as_());
|
||||
let end = site2.wpos_tile_pos(plaza.root_tile());
|
||||
|
||||
let path = match path_in_site(start, end, site2) {
|
||||
PathResult::Path(path) => path,
|
||||
_ => return None,
|
||||
};
|
||||
println!("CHOSE PATH, len = {}, start = {:?}, end = {:?}\nnpc = {:?}", path.len(), start, end, d.npc_id);
|
||||
Some((current_site.world_site?, path))
|
||||
});
|
||||
|
||||
if let Some((site, path)) = path {
|
||||
println!("Begin path");
|
||||
task.perform(0, walk_path(site, path));
|
||||
} else {
|
||||
println!("No path, waiting...");
|
||||
for _ in 0..100 {
|
||||
yield ();
|
||||
}
|
||||
println!("Waited.");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn walk_path(site: Id<WorldSite>, path: Path<Vec2<i32>>) -> impl IsTask {
|
||||
move |mut data: Data<NpcData>| {
|
||||
for tile in path {
|
||||
println!("TILE");
|
||||
let wpos = data.with(|d| match &d.ctx.index.sites.get(site).kind {
|
||||
SiteKind::Refactor(site2)
|
||||
| SiteKind::CliffTown(site2)
|
||||
| SiteKind::DesertCity(site2) => Some(site2),
|
||||
_ => None,
|
||||
}
|
||||
.expect("intrasite path should only be started on a site2 site")
|
||||
.tile_center_wpos(tile)
|
||||
.as_()
|
||||
+ 0.5);
|
||||
|
||||
println!("Walking to next tile... tile wpos = {:?} npc wpos = {:?}", wpos, data.with(|d| d.npc.wpos));
|
||||
while data.with(|d| d.npc.wpos.xy().distance_squared(wpos) > 2.0) {
|
||||
data.with(|d| d.controller.goto = Some((
|
||||
wpos.with_z(d.ctx.world
|
||||
.sim()
|
||||
.get_alt_approx(wpos.map(|e| e as i32))
|
||||
.unwrap_or(0.0)),
|
||||
1.0,
|
||||
)));
|
||||
yield ();
|
||||
}
|
||||
}
|
||||
|
||||
println!("Waiting..");
|
||||
for _ in 0..100 {
|
||||
yield ();
|
||||
}
|
||||
println!("Waited.");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user