Cleaned up rtsim tick handling

This commit is contained in:
Joshua Barretto 2023-04-05 16:32:54 +01:00
parent 80e4e8deae
commit 74610833d0
11 changed files with 100 additions and 53 deletions

View File

@ -219,12 +219,14 @@ pub enum NpcActivity {
Dance, Dance,
} }
/// Represents event-like actions that rtsim NPCs can perform to interact with
/// the world
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum NpcAction { pub enum NpcAction {
Greet(Actor), /// Speak the given message, with an optional target for that speech.
// TODO: Use some sort of structured, language-independent value that frontends can translate // TODO: Use some sort of structured, language-independent value that frontends can translate
// instead // instead
Say(Cow<'static, str>), Say(Option<Actor>, Cow<'static, str>),
} }
// Note: the `serde(name = "...")` is to minimise the length of field // Note: the `serde(name = "...")` is to minimise the length of field

View File

@ -10,7 +10,7 @@ pub struct Faction {
pub good_or_evil: bool, // TODO: Very stupid, get rid of this pub good_or_evil: bool, // TODO: Very stupid, get rid of this
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Default, Serialize, Deserialize)]
pub struct Factions { pub struct Factions {
pub factions: HopSlotMap<FactionId, Faction>, pub factions: HopSlotMap<FactionId, Faction>,
} }

View File

@ -23,12 +23,20 @@ use std::{
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Data { pub struct Data {
pub nature: Nature, pub nature: Nature,
#[serde(default)]
pub npcs: Npcs, pub npcs: Npcs,
#[serde(default)]
pub sites: Sites, pub sites: Sites,
#[serde(default)]
pub factions: Factions, pub factions: Factions,
#[serde(default)]
pub tick: u64,
#[serde(default)]
pub time_of_day: TimeOfDay, pub time_of_day: TimeOfDay,
// If true, rtsim data will be ignored (and, hence, overwritten on next save) on load. // If true, rtsim data will be ignored (and, hence, overwritten on next save) on load.
#[serde(default)]
pub should_purge: bool, pub should_purge: bool,
} }

View File

@ -4,7 +4,8 @@ use common::{
comp, comp,
grid::Grid, grid::Grid,
rtsim::{ rtsim::{
Actor, ChunkResource, FactionId, NpcAction, NpcActivity, Personality, SiteId, VehicleId, Actor, ChunkResource, FactionId, NpcAction, NpcActivity, NpcMsg, Personality, SiteId,
VehicleId,
}, },
store::Id, store::Id,
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
@ -69,10 +70,8 @@ impl Controller {
pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); } pub fn do_dance(&mut self) { self.activity = Some(NpcActivity::Dance); }
pub fn greet(&mut self, actor: Actor) { self.actions.push(NpcAction::Greet(actor)); } pub fn say(&mut self, target: impl Into<Option<Actor>>, msg: impl Into<Cow<'static, str>>) {
self.actions.push(NpcAction::Say(target.into(), msg.into()));
pub fn say(&mut self, msg: impl Into<Cow<'static, str>>) {
self.actions.push(NpcAction::Say(msg.into()));
} }
} }
@ -206,6 +205,7 @@ pub enum VehicleKind {
Boat, Boat,
} }
// TODO: Merge into `Npc`?
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
pub struct Vehicle { pub struct Vehicle {
pub wpos: Vec3<f32>, pub wpos: Vec3<f32>,
@ -273,6 +273,17 @@ pub struct Npcs {
pub character_map: HashMap<Vec2<i32>, Vec<(common::character::CharacterId, Vec3<f32>)>>, pub character_map: HashMap<Vec2<i32>, Vec<(common::character::CharacterId, Vec3<f32>)>>,
} }
impl Default for Npcs {
fn default() -> Self {
Self {
npcs: Default::default(),
vehicles: Default::default(),
npc_grid: construct_npc_grid(),
character_map: Default::default(),
}
}
}
fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) } fn construct_npc_grid() -> Grid<GridCell> { Grid::new(Vec2::zero(), Default::default()) }
impl Npcs { impl Npcs {

View File

@ -42,7 +42,7 @@ impl Site {
} }
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Default, Serialize, Deserialize)]
pub struct Sites { pub struct Sites {
pub sites: HopSlotMap<SiteId, Site>, pub sites: HopSlotMap<SiteId, Site>,

View File

@ -20,6 +20,7 @@ impl Event for OnSetup {}
pub struct OnTick { pub struct OnTick {
pub time_of_day: TimeOfDay, pub time_of_day: TimeOfDay,
pub time: Time, pub time: Time,
pub tick: u64,
pub dt: f32, pub dt: f32,
} }
impl Event for OnTick {} impl Event for OnTick {}

View File

@ -44,6 +44,7 @@ impl Data {
factions: Default::default(), factions: Default::default(),
}, },
tick: 0,
time_of_day: TimeOfDay(settings.start_time), time_of_day: TimeOfDay(settings.start_time),
should_purge: false, should_purge: false,
}; };

View File

@ -150,9 +150,15 @@ impl RtState {
time: Time, time: Time,
dt: f32, dt: f32,
) { ) {
self.data_mut().time_of_day = time_of_day; let tick = {
let mut data = self.data_mut();
data.time_of_day = time_of_day;
data.tick += 1;
data.tick
};
let event = OnTick { let event = OnTick {
time_of_day, time_of_day,
tick,
time, time,
dt, dt,
}; };

View File

@ -3,7 +3,7 @@ use std::hash::BuildHasherDefault;
use crate::{ use crate::{
ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx}, ai::{casual, choose, finish, important, just, now, seq, until, Action, NpcCtx},
data::{ data::{
npc::{Brain, PathData}, npc::{Brain, PathData, SimulationMode},
Sites, Sites,
}, },
event::OnTick, event::OnTick,
@ -33,6 +33,12 @@ use world::{
IndexRef, World, IndexRef, World,
}; };
/// How many ticks should pass between running NPC AI.
/// Note that this only applies to simulated NPCs: loaded NPCs have their AI
/// code run every tick. This means that AI code should be broadly
/// DT-independent.
const SIMULATED_TICK_SKIP: u64 = 10;
pub struct NpcAi; pub struct NpcAi;
const CARDINALS: &[Vec2<i32>] = &[ const CARDINALS: &[Vec2<i32>] = &[
@ -209,6 +215,8 @@ impl Rule for NpcAi {
let mut data = ctx.state.data_mut(); let mut data = ctx.state.data_mut();
data.npcs data.npcs
.iter_mut() .iter_mut()
// Don't run AI for simulated NPCs every tick
.filter(|(_, npc)| matches!(npc.mode, SimulationMode::Loaded) || (npc.seed as u64 + ctx.event.tick) % SIMULATED_TICK_SKIP == 0)
.map(|(npc_id, npc)| { .map(|(npc_id, npc)| {
let controller = std::mem::take(&mut npc.controller); let controller = std::mem::take(&mut npc.controller);
let brain = npc.brain.take().unwrap_or_else(|| Brain { let brain = npc.brain.take().unwrap_or_else(|| Brain {
@ -459,7 +467,7 @@ fn socialize() -> impl Action {
.nearby(Some(ctx.npc_id), ctx.npc.wpos.xy(), 8.0) .nearby(Some(ctx.npc_id), ctx.npc.wpos.xy(), 8.0)
.choose(&mut ctx.rng) .choose(&mut ctx.rng)
{ {
just(move |ctx| ctx.controller.greet(other)).boxed() just(move |ctx| ctx.controller.say(other, "npc-speech-villager_open")).boxed()
} else if ctx.rng.gen_bool(0.0003) { } else if ctx.rng.gen_bool(0.0003) {
just(|ctx| ctx.controller.do_dance()) just(|ctx| ctx.controller.do_dance())
.repeat() .repeat()
@ -506,7 +514,7 @@ fn adventure() -> impl Action {
.map(|ws| ctx.index.sites.get(ws).name().to_string()) .map(|ws| ctx.index.sites.get(ws).name().to_string())
.unwrap_or_default(); .unwrap_or_default();
// Travel to the site // Travel to the site
important(just(move |ctx| ctx.controller.say(format!("I've spent enough time here, onward to {}!", site_name))) important(just(move |ctx| ctx.controller.say(None, format!("I've spent enough time here, onward to {}!", site_name)))
.then(travel_to_site(tgt_site, 0.6)) .then(travel_to_site(tgt_site, 0.6))
// Stop for a few minutes // Stop for a few minutes
.then(villager(tgt_site).repeat().stop_if(timeout(wait_time))) .then(villager(tgt_site).repeat().stop_if(timeout(wait_time)))
@ -596,12 +604,12 @@ fn villager(visiting_site: SiteId) -> impl Action {
Some(site2.tile_center_wpos(house.root_tile()).as_()) Some(site2.tile_center_wpos(house.root_tile()).as_())
}) })
{ {
just(|ctx| ctx.controller.say("It's dark, time to go home")) just(|ctx| ctx.controller.say(None, "It's dark, time to go home"))
.then(travel_to_point(house_wpos, 0.65)) .then(travel_to_point(house_wpos, 0.65))
.debug(|| "walk to house") .debug(|| "walk to house")
.then(socialize().repeat().debug(|| "wait in house")) .then(socialize().repeat().debug(|| "wait in house"))
.stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light()) .stop_if(|ctx| DayPeriod::from(ctx.time_of_day.0).is_light())
.then(just(|ctx| ctx.controller.say("A new day begins!"))) .then(just(|ctx| ctx.controller.say(None, "A new day begins!")))
.map(|_| ()) .map(|_| ())
.boxed() .boxed()
} else { } else {
@ -627,7 +635,7 @@ fn villager(visiting_site: SiteId) -> impl Action {
} else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) { } else if matches!(ctx.npc.profession, Some(Profession::Hunter)) && ctx.rng.gen_bool(0.8) {
if let Some(forest_wpos) = find_forest(ctx) { if let Some(forest_wpos) = find_forest(ctx) {
return casual( return casual(
just(|ctx| ctx.controller.say("Time to go hunting!")) just(|ctx| ctx.controller.say(None, "Time to go hunting!"))
.then(travel_to_point(forest_wpos, 0.75)) .then(travel_to_point(forest_wpos, 0.75))
.debug(|| "walk to forest") .debug(|| "walk to forest")
.then({ .then({
@ -641,17 +649,33 @@ fn villager(visiting_site: SiteId) -> impl Action {
{ {
return casual( return casual(
just(|ctx| { just(|ctx| {
ctx.controller.say( // Try to direct our speech at nearby actors, if there are any
*[ let (target, phrases) = if ctx.rng.gen_bool(0.3) && let Some(other) = ctx
.state
.data()
.npcs
.nearby(Some(ctx.npc_id), ctx.npc.wpos.xy(), 8.0)
.choose(&mut ctx.rng)
{
(Some(other), &[
"You there! Are you in need of a new thingamabob?",
"Are you hungry? I'm sure I've got some cheese you can buy",
"You look like you could do with some new armour!",
][..])
// Otherwise, resort to generic expressions
} else {
(None, &[
"All my goods are of the highest quality!", "All my goods are of the highest quality!",
"Does anybody want to buy my wares?", "Does anybody want to buy my wares?",
"I've got the best offers in town.", "I've got the best offers in town",
"Looking for supplies? I've got you covered.", "Looking for supplies? I've got you covered",
] ][..])
.iter() };
.choose(&mut ctx.rng)
.unwrap(), ctx.controller.say(
) // Can't fail target,
*phrases.iter().choose(&mut ctx.rng).unwrap(), // Can't fail
);
}) })
.then(idle().repeat().stop_if(timeout(8.0))) .then(idle().repeat().stop_if(timeout(8.0)))
.repeat() .repeat()

View File

@ -244,8 +244,7 @@ fn on_tick(ctx: EventCtx<SimulateNpcs, OnTick>) {
// Consume NPC actions // Consume NPC actions
for action in std::mem::take(&mut npc.controller.actions) { for action in std::mem::take(&mut npc.controller.actions) {
match action { match action {
NpcAction::Greet(_) | NpcAction::Say(_) => {}, /* Currently, just swallow NpcAction::Say(_, _) => {}, // Currently, just swallow interactions
* interactions */
} }
} }

View File

@ -474,36 +474,31 @@ fn set_owner_if_no_target(bdata: &mut BehaviorData) -> bool {
fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool { fn handle_rtsim_actions(bdata: &mut BehaviorData) -> bool {
if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() { if let Some(action) = bdata.agent.rtsim_controller.actions.pop_front() {
match action { match action {
NpcAction::Greet(actor) => { NpcAction::Say(target, msg) => {
if bdata.agent.allowed_to_speak() if bdata.agent.allowed_to_speak() {
&& let Some(target) = bdata.read_data.lookup_actor(actor) // Aim the speech toward a target
{ if let Some(target) = target.and_then(|tgt| bdata.read_data.lookup_actor(tgt)) {
bdata.agent.target = Some(Target::new( bdata.agent.target = Some(Target::new(
target, target,
false, false,
bdata.read_data.time.0, bdata.read_data.time.0,
false, false,
bdata.read_data.positions.get(target).map(|p| p.0), bdata.read_data.positions.get(target).map(|p| p.0),
)); ));
// We're always aware of someone we're talking to // We're always aware of someone we're talking to
bdata.agent.awareness.set_maximally_aware(); bdata.agent.awareness.set_maximally_aware();
// Start a timer so that we eventually stop interacting
bdata
.agent
.timer
.start(bdata.read_data.time.0, TimerAction::Interact);
bdata.controller.push_action(ControlAction::Stand);
}
bdata.controller.push_action(ControlAction::Stand);
bdata.controller.push_utterance(UtteranceKind::Greeting); bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata bdata.agent_data.chat_npc(msg, bdata.event_emitter);
.agent_data
.chat_npc("npc-speech-villager_open", bdata.event_emitter);
// Start a timer so that they eventually stop interacting
bdata
.agent
.timer
.start(bdata.read_data.time.0, TimerAction::Interact);
} }
}, },
NpcAction::Say(msg) => {
bdata.controller.push_utterance(UtteranceKind::Greeting);
bdata.agent_data.chat_npc(msg, bdata.event_emitter);
},
} }
true true
} else { } else {