Add fundamentals for wiring system.

This commit is contained in:
Daniel Mizerski 2021-04-15 18:28:16 +02:00
parent 247ed4d78a
commit 7faa0d3cd9
10 changed files with 499 additions and 2 deletions

View File

@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Lift is now calculated for gliders based on dimensions (currently same for all) - Lift is now calculated for gliders based on dimensions (currently same for all)
- Specific music tracks can now play exclusively in towns. - Specific music tracks can now play exclusively in towns.
- Custom map markers can be placed now - Custom map markers can be placed now
- Fundamentals/prototype for wiring system
### Changed ### Changed

View File

@ -94,6 +94,7 @@ pub enum ChatCommand {
Unban, Unban,
Version, Version,
Waypoint, Waypoint,
Wiring,
Whitelist, Whitelist,
World, World,
} }
@ -157,6 +158,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Unban, ChatCommand::Unban,
ChatCommand::Version, ChatCommand::Version,
ChatCommand::Waypoint, ChatCommand::Waypoint,
ChatCommand::Wiring,
ChatCommand::Whitelist, ChatCommand::Whitelist,
ChatCommand::World, ChatCommand::World,
]; ];
@ -529,6 +531,7 @@ impl ChatCommand {
ChatCommand::Waypoint => { ChatCommand::Waypoint => {
cmd(vec![], "Set your waypoint to your current position", Admin) cmd(vec![], "Set your waypoint to your current position", Admin)
}, },
ChatCommand::Wiring => cmd(vec![], "Create wiring element", Admin),
ChatCommand::Whitelist => cmd( ChatCommand::Whitelist => cmd(
vec![Any("add/remove", Required), Any("username", Required)], vec![Any("add/remove", Required), Any("username", Required)],
"Adds/removes username to whitelist", "Adds/removes username to whitelist",
@ -602,6 +605,7 @@ impl ChatCommand {
ChatCommand::Unban => "unban", ChatCommand::Unban => "unban",
ChatCommand::Version => "version", ChatCommand::Version => "version",
ChatCommand::Waypoint => "waypoint", ChatCommand::Waypoint => "waypoint",
ChatCommand::Wiring => "wiring",
ChatCommand::Whitelist => "whitelist", ChatCommand::Whitelist => "whitelist",
ChatCommand::World => "world", ChatCommand::World => "world",
} }

View File

@ -39,11 +39,13 @@ use core::{convert::TryFrom, ops::Not, time::Duration};
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::Rng; use rand::Rng;
use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement};
use hashbrown::HashMap;
use vek::*; use vek::*;
use world::util::Sampler; use world::util::Sampler;
use crate::{client::Client, login_provider::LoginProvider}; use crate::{client::Client, login_provider::LoginProvider, wiring};
use scan_fmt::{scan_fmt, scan_fmt_some}; use scan_fmt::{scan_fmt, scan_fmt_some};
use tracing::{error, info, warn}; use tracing::{error, info, warn};
@ -149,6 +151,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Unban => handle_unban, ChatCommand::Unban => handle_unban,
ChatCommand::Version => handle_version, ChatCommand::Version => handle_version,
ChatCommand::Waypoint => handle_waypoint, ChatCommand::Waypoint => handle_waypoint,
ChatCommand::Wiring => handle_spawn_wiring,
ChatCommand::Whitelist => handle_whitelist, ChatCommand::Whitelist => handle_whitelist,
ChatCommand::World => handle_world, ChatCommand::World => handle_world,
} }
@ -1702,6 +1705,102 @@ fn handle_waypoint(
Ok(()) Ok(())
} }
fn handle_spawn_wiring(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
_args: String,
_action: &ChatCommand,
) -> CmdResult<()> {
// Obviously it is a WIP - use it for debug
let mut pos = position(server, target, "target")?;
pos.0.z += 1.0;
pos.0.x += 3.0;
let mut outputs1 = HashMap::new();
outputs1.insert(String::from("color"), wiring::OutputFormula::OnCollide {
value: 1.0,
});
let builder1 = server
.state
.create_wiring(pos, comp::object::Body::Coins, WiringElement {
actions: vec![WiringAction {
formula: wiring::OutputFormula::Constant { value: 1.0 },
threshold: 1.0,
effects: vec![WiringActionEffect::SetLight {
r: wiring::OutputFormula::Input {
name: String::from("color"),
},
g: wiring::OutputFormula::Input {
name: String::from("color"),
},
b: wiring::OutputFormula::Input {
name: String::from("color"),
},
}],
}],
inputs: HashMap::new(),
outputs: outputs1,
});
let ent1 = builder1.build();
pos.0.x += 3.0;
let builder2 = server
.state
.create_wiring(pos, comp::object::Body::Coins, WiringElement {
actions: vec![WiringAction {
formula: wiring::OutputFormula::Input {
name: String::from("color"),
},
threshold: 1.0,
effects: vec![
// Another demo:
// WiringActionEffect::SetLight {
// r: wiring::OutputFormula::Input { name: String::from("color") },
// g: wiring::OutputFormula::Input { name: String::from("color") },
// b: wiring::OutputFormula::Input { name: String::from("color") }
// },
WiringActionEffect::SpawnProjectile {
constr: comp::ProjectileConstructor::Arrow {
damage: 1.0,
energy_regen: 0.0,
knockback: 0.0,
},
},
],
}],
inputs: HashMap::new(),
outputs: HashMap::new(),
});
let ent2 = builder2.build();
pos.0.x += 3.0;
let builder3 = server
.state
.create_wiring(pos, comp::object::Body::TrainingDummy, WiringElement {
actions: vec![],
inputs: HashMap::new(),
outputs: HashMap::new(),
})
.with(Circuit {
wires: vec![Wire {
input_entity: ent1,
input_field: String::from("color"),
output_entity: ent2,
output_field: String::from("color"),
}],
});
builder3.build();
server.notify_client(
client,
ServerGeneral::server_msg(ChatType::CommandInfo, "Wire"),
);
Ok(())
}
#[allow(clippy::useless_conversion)] // TODO: Pending review in #587 #[allow(clippy::useless_conversion)] // TODO: Pending review in #587
fn handle_adminify( fn handle_adminify(
server: &mut Server, server: &mut Server,

View File

@ -30,6 +30,7 @@ pub mod settings;
pub mod state_ext; pub mod state_ext;
pub mod sys; pub mod sys;
#[cfg(not(feature = "worldgen"))] mod test_world; #[cfg(not(feature = "worldgen"))] mod test_world;
pub mod wiring;
// Reexports // Reexports
pub use crate::{ pub use crate::{
@ -242,6 +243,8 @@ impl Server {
state.ecs_mut().register::<RegionSubscription>(); state.ecs_mut().register::<RegionSubscription>();
state.ecs_mut().register::<Client>(); state.ecs_mut().register::<Client>();
state.ecs_mut().register::<Presence>(); state.ecs_mut().register::<Presence>();
state.ecs_mut().register::<wiring::WiringElement>();
state.ecs_mut().register::<wiring::Circuit>();
state.ecs_mut().register::<comp::HomeChunk>(); state.ecs_mut().register::<comp::HomeChunk>();
state.ecs_mut().register::<login_provider::PendingLogin>(); state.ecs_mut().register::<login_provider::PendingLogin>();

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
client::Client, persistence::PersistedComponents, presence::Presence, settings::Settings, client::Client, persistence::PersistedComponents, presence::Presence, settings::Settings,
sys::sentinel::DeletedEntities, SpawnPoint, sys::sentinel::DeletedEntities, wiring, SpawnPoint,
}; };
use common::{ use common::{
character::CharacterId, character::CharacterId,
@ -76,6 +76,12 @@ pub trait StateExt {
) -> EcsEntityBuilder; ) -> EcsEntityBuilder;
/// Creates a safezone /// Creates a safezone
fn create_safezone(&mut self, range: Option<f32>, pos: comp::Pos) -> EcsEntityBuilder; fn create_safezone(&mut self, range: Option<f32>, pos: comp::Pos) -> EcsEntityBuilder;
fn create_wiring(
&mut self,
pos: comp::Pos,
object: comp::object::Body,
wiring_element: wiring::WiringElement,
) -> EcsEntityBuilder;
// NOTE: currently only used for testing // NOTE: currently only used for testing
/// Queues chunk generation in the view distance of the persister, this /// Queues chunk generation in the view distance of the persister, this
/// entity must be built before those chunks are received (the builder /// entity must be built before those chunks are received (the builder
@ -347,6 +353,34 @@ impl StateExt for State {
)])) )]))
} }
fn create_wiring(
&mut self,
pos: comp::Pos,
object: comp::object::Body,
wiring_element: wiring::WiringElement,
) -> EcsEntityBuilder {
self.ecs_mut()
.create_entity_synced()
.with(pos)
.with(comp::Vel(Vec3::zero()))
.with(comp::Ori::default())
.with(comp::Collider::Box {
radius: comp::Body::Object(object).radius(),
z_min: 0.0,
z_max: comp::Body::Object(object).height()
})
.with(comp::Body::Object(object))
.with(comp::Mass(10.0))
// .with(comp::Sticky)
.with(wiring_element)
.with(comp::LightEmitter {
col: Rgb::new(0.0, 0.0, 0.0),
strength: 2.0,
flicker: 1.0,
animated: true,
})
}
// NOTE: currently only used for testing // NOTE: currently only used for testing
/// Queues chunk generation in the view distance of the persister, this /// Queues chunk generation in the view distance of the persister, this
/// entity must be built before those chunks are received (the builder /// entity must be built before those chunks are received (the builder

View File

@ -10,6 +10,7 @@ pub mod subscription;
pub mod terrain; pub mod terrain;
pub mod terrain_sync; pub mod terrain_sync;
pub mod waypoint; pub mod waypoint;
pub mod wiring;
use common_ecs::{dispatch, run_now, System}; use common_ecs::{dispatch, run_now, System};
use common_systems::{melee, projectile}; use common_systems::{melee, projectile};
@ -30,6 +31,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch::<invite_timeout::Sys>(dispatch_builder, &[]); dispatch::<invite_timeout::Sys>(dispatch_builder, &[]);
dispatch::<persistence::Sys>(dispatch_builder, &[]); dispatch::<persistence::Sys>(dispatch_builder, &[]);
dispatch::<object::Sys>(dispatch_builder, &[]); dispatch::<object::Sys>(dispatch_builder, &[]);
dispatch::<wiring::Sys>(dispatch_builder, &[]);
} }
pub fn run_sync_systems(ecs: &mut specs::World) { pub fn run_sync_systems(ecs: &mut specs::World) {

75
server/src/sys/wiring.rs Normal file
View File

@ -0,0 +1,75 @@
use crate::wiring::{Circuit, WiringElement};
use common::{
comp::{LightEmitter, PhysicsState},
event::{EventBus, ServerEvent},
};
use common_ecs::{Job, Origin, Phase, System};
use hashbrown::HashMap;
use specs::{
join::Join, shred::ResourceId, Entities, Entity, Read, ReadStorage, SystemData, World,
WriteStorage,
};
mod compute_outputs;
use compute_outputs::compute_outputs;
mod dispatch_actions;
use dispatch_actions::dispatch_actions;
#[derive(SystemData)]
pub struct WiringData<'a> {
pub entities: Entities<'a>,
pub event_bus: Read<'a, EventBus<ServerEvent>>,
pub circuits: ReadStorage<'a, Circuit>,
pub wiring_elements: WriteStorage<'a, WiringElement>,
pub light_emitters: WriteStorage<'a, LightEmitter>,
pub physics_states: ReadStorage<'a, PhysicsState>,
}
/// This system is responsible for handling wiring (signals and wiring systems)
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = WiringData<'a>;
const NAME: &'static str = "wiring";
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, mut system_data: Self::SystemData) {
// Calculate new outputs using inputs (those inputs are calculated and populated
// in previous tick) Take inputs and wiring_element.outputs and with
// that compute new outputs
let computed_outputs = compute_outputs(&system_data);
// Pass new outputs as inputs for the next tick
dispatch_circuit_transport(&computed_outputs, &mut system_data);
// Using inputs dispatch actions
dispatch_actions(&mut system_data);
}
}
fn dispatch_circuit_transport<'a>(
computed_outputs: &HashMap<Entity, HashMap<String, f32>>,
system_data: &mut WiringData<'a>,
) {
let WiringData {
circuits,
wiring_elements,
..
} = system_data;
(circuits)
.join()
.map(|circuit| circuit.wires.iter())
.flatten()
.for_each(|wire| {
let input_value = computed_outputs
.get(&wire.input_entity)
.and_then(|e| e.get(&wire.input_field))
.unwrap_or(&0.0);
if let Some(wiring_element) = wiring_elements.get_mut(wire.output_entity) {
wiring_element
.inputs
.insert(wire.output_field.clone(), *input_value);
}
});
}

View File

@ -0,0 +1,94 @@
use super::WiringData;
use crate::wiring::OutputFormula;
use common::comp::PhysicsState;
use hashbrown::HashMap;
use specs::{join::Join, Entity, ReadStorage};
use tracing::warn;
pub fn compute_outputs(system_data: &WiringData) -> HashMap<Entity, HashMap<String, f32>> {
let WiringData {
entities,
wiring_elements,
physics_states,
..
} = system_data;
(&*entities, wiring_elements)
.join()
.map(|(entity, wiring_element)| {
(
entity,
wiring_element
.outputs
.iter()
.map(
|(key, output_formula)| {
compute_output_with_key(
key,
output_formula,
&wiring_element.inputs,
entity,
physics_states,
)
}, // (String, f32)
)
.collect::<HashMap<_, _>>(),
)
})
.collect()
}
pub fn compute_output_with_key(
// yes, this function is defined only to make one place
// look a bit nicer
// Don't discuss.
key: &str,
output_formula: &OutputFormula,
inputs: &HashMap<String, f32>,
entity: Entity,
physics_states: &ReadStorage<PhysicsState>,
) -> (String, f32) {
(
key.to_string(),
compute_output(output_formula, inputs, entity, physics_states),
)
}
pub fn compute_output(
output_formula: &OutputFormula,
inputs: &HashMap<String, f32>,
entity: Entity,
physics_states: &ReadStorage<PhysicsState>,
) -> f32 {
match output_formula {
OutputFormula::Constant { value } => *value,
OutputFormula::Input { name } => *inputs.get(name).unwrap_or(&0.0),
OutputFormula::Logic(_logic) => {
warn!("Not implemented OutputFormula::Logic");
0.0
},
OutputFormula::SineWave { .. } => {
warn!("Not implemented OutputFormula::SineWave");
0.0
},
OutputFormula::OnCollide { value } => {
output_formula_on_collide(value, entity, physics_states)
},
OutputFormula::OnInteract { .. } => {
warn!("Not implemented OutputFormula::OnInteract");
0.0
},
}
}
fn output_formula_on_collide(
value: &f32,
entity: Entity,
physics_states: &ReadStorage<PhysicsState>,
) -> f32 {
if let Some(ps) = physics_states.get(entity) {
if !ps.touch_entities.is_empty() {
return *value;
}
}
0.0
}

View File

@ -0,0 +1,111 @@
use super::{compute_outputs::compute_output, WiringData};
use crate::wiring::{OutputFormula, WiringActionEffect};
use common::{
comp::{object, Body, LightEmitter, PhysicsState, ProjectileConstructor},
event::{Emitter, ServerEvent},
util::Dir,
};
use hashbrown::HashMap;
use specs::{join::Join, Entity, ReadStorage, WriteStorage};
use tracing::warn;
use vek::Rgb;
pub fn dispatch_actions(system_data: &mut WiringData) {
let WiringData {
entities,
event_bus,
wiring_elements,
light_emitters,
physics_states,
..
} = system_data;
let mut server_emitter = event_bus.emitter();
(&*entities, wiring_elements)
.join()
.for_each(|(entity, wiring_element)| {
wiring_element
.actions
.iter()
.filter(|wiring_action| {
compute_output(
&wiring_action.formula,
&wiring_element.inputs,
entity,
physics_states,
) >= wiring_action.threshold
})
.for_each(|wiring_action| {
dispatch_action(
entity,
&wiring_element.inputs,
&wiring_action.effects,
&mut server_emitter,
light_emitters,
physics_states,
);
})
})
}
fn dispatch_action(
entity: Entity,
source: &HashMap<String, f32>,
action_effects: &[WiringActionEffect],
server_emitter: &mut Emitter<ServerEvent>,
light_emitters: &mut WriteStorage<LightEmitter>,
physics_states: &ReadStorage<PhysicsState>,
) {
action_effects
.iter()
.for_each(|action_effect| match action_effect {
WiringActionEffect::SetBlockCollidability { .. } => {
warn!("Not implemented WiringActionEffect::SetBlockCollidability")
},
WiringActionEffect::SpawnProjectile { constr } => {
dispatch_action_spawn_projectile(entity, constr, server_emitter)
},
WiringActionEffect::SetLight { r, g, b } => {
dispatch_action_set_light(entity, source, r, g, b, light_emitters, physics_states)
},
});
}
fn dispatch_action_spawn_projectile(
entity: Entity,
constr: &ProjectileConstructor,
server_emitter: &mut Emitter<ServerEvent>,
) {
// Use match here if there will be more options
// NOTE: constr in RFC is about Arrow projectile
server_emitter.emit(ServerEvent::Shoot {
entity,
dir: Dir::forward(),
body: Body::Object(object::Body::Arrow),
projectile: constr.create_projectile(None, 1.0, 1.0),
light: None,
speed: 5.0,
object: None,
});
}
fn dispatch_action_set_light(
entity: Entity,
source: &HashMap<String, f32>,
r: &OutputFormula,
g: &OutputFormula,
b: &OutputFormula,
light_emitters: &mut WriteStorage<LightEmitter>,
physics_states: &ReadStorage<PhysicsState>,
) {
let mut light_emitter = light_emitters.get_mut(entity).unwrap();
// TODO: make compute_output accept multiple formulas
let computed_r = compute_output(r, source, entity, physics_states);
let computed_g = compute_output(g, source, entity, physics_states);
let computed_b = compute_output(b, source, entity, physics_states);
light_emitter.col = Rgb::new(computed_r, computed_g, computed_b);
}

74
server/src/wiring.rs Normal file
View File

@ -0,0 +1,74 @@
use common::comp::ProjectileConstructor;
use core::f32;
use hashbrown::HashMap;
use specs::{Component, Entity};
use specs_idvs::IdvStorage;
use vek::Vec3;
pub struct Logic {
pub kind: LogicKind,
pub left: OutputFormula,
pub right: OutputFormula,
}
pub struct WiringElement {
pub inputs: HashMap<String, f32>,
pub outputs: HashMap<String, OutputFormula>,
pub actions: Vec<WiringAction>,
}
pub struct Circuit {
pub wires: Vec<Wire>,
}
pub enum OutputFormula {
Constant { value: f32 },
Input { name: String },
Logic(Box<Logic>),
SineWave { amplitude: f32, frequency: f32 },
OnCollide { value: f32 },
OnInteract { value: f32 },
}
pub enum LogicKind {
Min, // acts like And
Max, // acts like Or
Sub, // `|x| { 5.0 - x }` acts like Not, depending on reference voltages
}
pub struct WiringAction {
pub formula: OutputFormula,
pub threshold: f32,
pub effects: Vec<WiringActionEffect>,
}
pub enum WiringActionEffect {
SpawnProjectile {
constr: ProjectileConstructor,
},
SetBlockCollidability {
coords: Vec3<i32>,
collidable: bool,
},
SetLight {
r: OutputFormula,
g: OutputFormula,
b: OutputFormula,
},
}
pub struct Wire {
pub input_field: String,
pub output_field: String,
pub input_entity: Entity,
pub output_entity: Entity,
}
impl Component for WiringElement {
type Storage = IdvStorage<Self>;
}
impl Component for Circuit {
type Storage = IdvStorage<Self>;
}