From 7faa0d3cd949d7dae9d247f32dd6350a1c2aba06 Mon Sep 17 00:00:00 2001 From: Daniel Mizerski Date: Thu, 15 Apr 2021 18:28:16 +0200 Subject: [PATCH] Add fundamentals for wiring system. --- CHANGELOG.md | 1 + common/src/cmd.rs | 4 + server/src/cmd.rs | 101 +++++++++++++++++++- server/src/lib.rs | 3 + server/src/state_ext.rs | 36 ++++++- server/src/sys/mod.rs | 2 + server/src/sys/wiring.rs | 75 +++++++++++++++ server/src/sys/wiring/compute_outputs.rs | 94 ++++++++++++++++++ server/src/sys/wiring/dispatch_actions.rs | 111 ++++++++++++++++++++++ server/src/wiring.rs | 74 +++++++++++++++ 10 files changed, 499 insertions(+), 2 deletions(-) create mode 100644 server/src/sys/wiring.rs create mode 100644 server/src/sys/wiring/compute_outputs.rs create mode 100644 server/src/sys/wiring/dispatch_actions.rs create mode 100644 server/src/wiring.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ee707f8e9d..7b8739a38c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) - Specific music tracks can now play exclusively in towns. - Custom map markers can be placed now +- Fundamentals/prototype for wiring system ### Changed diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 452213418a..67bb0b4218 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -94,6 +94,7 @@ pub enum ChatCommand { Unban, Version, Waypoint, + Wiring, Whitelist, World, } @@ -157,6 +158,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Unban, ChatCommand::Version, ChatCommand::Waypoint, + ChatCommand::Wiring, ChatCommand::Whitelist, ChatCommand::World, ]; @@ -529,6 +531,7 @@ impl ChatCommand { ChatCommand::Waypoint => { cmd(vec![], "Set your waypoint to your current position", Admin) }, + ChatCommand::Wiring => cmd(vec![], "Create wiring element", Admin), ChatCommand::Whitelist => cmd( vec![Any("add/remove", Required), Any("username", Required)], "Adds/removes username to whitelist", @@ -602,6 +605,7 @@ impl ChatCommand { ChatCommand::Unban => "unban", ChatCommand::Version => "version", ChatCommand::Waypoint => "waypoint", + ChatCommand::Wiring => "wiring", ChatCommand::Whitelist => "whitelist", ChatCommand::World => "world", } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 47081c6a48..753ec84dd8 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -39,11 +39,13 @@ use core::{convert::TryFrom, ops::Not, time::Duration}; use hashbrown::HashSet; use rand::Rng; use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; +use wiring::{Circuit, Wire, WiringAction, WiringActionEffect, WiringElement}; +use hashbrown::HashMap; use vek::*; 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 tracing::{error, info, warn}; @@ -149,6 +151,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Unban => handle_unban, ChatCommand::Version => handle_version, ChatCommand::Waypoint => handle_waypoint, + ChatCommand::Wiring => handle_spawn_wiring, ChatCommand::Whitelist => handle_whitelist, ChatCommand::World => handle_world, } @@ -1702,6 +1705,102 @@ fn handle_waypoint( 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 fn handle_adminify( server: &mut Server, diff --git a/server/src/lib.rs b/server/src/lib.rs index 692ae02c0e..ea089c05c2 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -30,6 +30,7 @@ pub mod settings; pub mod state_ext; pub mod sys; #[cfg(not(feature = "worldgen"))] mod test_world; +pub mod wiring; // Reexports pub use crate::{ @@ -242,6 +243,8 @@ impl Server { state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); + state.ecs_mut().register::(); + state.ecs_mut().register::(); state.ecs_mut().register::(); state.ecs_mut().register::(); diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 89a037d95b..84db6304a2 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -1,6 +1,6 @@ use crate::{ client::Client, persistence::PersistedComponents, presence::Presence, settings::Settings, - sys::sentinel::DeletedEntities, SpawnPoint, + sys::sentinel::DeletedEntities, wiring, SpawnPoint, }; use common::{ character::CharacterId, @@ -76,6 +76,12 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Creates a safezone fn create_safezone(&mut self, range: Option, 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 /// Queues chunk generation in the view distance of the persister, this /// 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 /// Queues chunk generation in the view distance of the persister, this /// entity must be built before those chunks are received (the builder diff --git a/server/src/sys/mod.rs b/server/src/sys/mod.rs index 9be00f150a..bdf98b6a22 100644 --- a/server/src/sys/mod.rs +++ b/server/src/sys/mod.rs @@ -10,6 +10,7 @@ pub mod subscription; pub mod terrain; pub mod terrain_sync; pub mod waypoint; +pub mod wiring; use common_ecs::{dispatch, run_now, System}; use common_systems::{melee, projectile}; @@ -30,6 +31,7 @@ pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { dispatch::(dispatch_builder, &[]); dispatch::(dispatch_builder, &[]); dispatch::(dispatch_builder, &[]); + dispatch::(dispatch_builder, &[]); } pub fn run_sync_systems(ecs: &mut specs::World) { diff --git a/server/src/sys/wiring.rs b/server/src/sys/wiring.rs new file mode 100644 index 0000000000..257e440177 --- /dev/null +++ b/server/src/sys/wiring.rs @@ -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>, + 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, 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>, + 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); + } + }); +} diff --git a/server/src/sys/wiring/compute_outputs.rs b/server/src/sys/wiring/compute_outputs.rs new file mode 100644 index 0000000000..34faac7e66 --- /dev/null +++ b/server/src/sys/wiring/compute_outputs.rs @@ -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> { + 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::>(), + ) + }) + .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, + entity: Entity, + physics_states: &ReadStorage, +) -> (String, f32) { + ( + key.to_string(), + compute_output(output_formula, inputs, entity, physics_states), + ) +} + +pub fn compute_output( + output_formula: &OutputFormula, + inputs: &HashMap, + entity: Entity, + physics_states: &ReadStorage, +) -> 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, +) -> f32 { + if let Some(ps) = physics_states.get(entity) { + if !ps.touch_entities.is_empty() { + return *value; + } + } + 0.0 +} diff --git a/server/src/sys/wiring/dispatch_actions.rs b/server/src/sys/wiring/dispatch_actions.rs new file mode 100644 index 0000000000..821b525c38 --- /dev/null +++ b/server/src/sys/wiring/dispatch_actions.rs @@ -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, + action_effects: &[WiringActionEffect], + + server_emitter: &mut Emitter, + light_emitters: &mut WriteStorage, + physics_states: &ReadStorage, +) { + 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, +) { + // 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, + r: &OutputFormula, + g: &OutputFormula, + b: &OutputFormula, + + light_emitters: &mut WriteStorage, + physics_states: &ReadStorage, +) { + 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); +} diff --git a/server/src/wiring.rs b/server/src/wiring.rs new file mode 100644 index 0000000000..8b9cb38e10 --- /dev/null +++ b/server/src/wiring.rs @@ -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, + pub outputs: HashMap, + pub actions: Vec, +} + +pub struct Circuit { + pub wires: Vec, +} + +pub enum OutputFormula { + Constant { value: f32 }, + Input { name: String }, + Logic(Box), + + 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, +} + +pub enum WiringActionEffect { + SpawnProjectile { + constr: ProjectileConstructor, + }, + SetBlockCollidability { + coords: Vec3, + 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; +} + +impl Component for Circuit { + type Storage = IdvStorage; +}