diff --git a/Cargo.toml b/Cargo.toml index 5d067862..55eec416 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,4 +47,3 @@ opt-level = 1 [profile.release] #debug = true -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/egregoria/src/engine_interaction.rs b/egregoria/src/engine_interaction.rs index 14996f39..1ff85df7 100644 --- a/egregoria/src/engine_interaction.rs +++ b/egregoria/src/engine_interaction.rs @@ -1,3 +1,11 @@ +use std::collections::BTreeMap; +use std::time::Instant; + +use serde::{Deserialize, Serialize}; + +use geom::{vec3, Vec2, OBB}; +use WorldCommand::*; + use crate::economy::Government; use crate::map::procgen::{load_parismap, load_testfield}; use crate::map::{ @@ -5,18 +13,14 @@ use crate::map::{ LightPolicy, LotID, Map, MapProject, ProjectKind, RoadID, Terrain, TurnPolicy, Zone, }; use crate::map_dynamic::{BuildingInfos, ParkingManagement}; +use crate::multiplayer::chat::Message; +use crate::multiplayer::MultiplayerState; use crate::transportation::testing_vehicles::RandomVehicles; use crate::transportation::train::{spawn_train, RailWagonKind}; use crate::transportation::{spawn_parked_vehicle_with_spot, unpark, VehicleKind}; use crate::utils::rand_provider::RandProvider; use crate::utils::time::{GameTime, Tick}; use crate::{Egregoria, EgregoriaOptions, Replay}; -use geom::{vec3, Vec2, OBB}; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::time::Instant; - -use WorldCommand::*; #[derive(Clone, Default)] pub struct WorldCommands { @@ -32,6 +36,9 @@ pub enum WorldCommand { MapRemoveRoad(RoadID), MapRemoveBuilding(BuildingID), MapBuildHouse(LotID), + SendMessage { + message: Message, + }, SpawnRandomCars { n_cars: usize, }, @@ -318,6 +325,12 @@ impl WorldCommand { goria.write::().vehicles.insert(v_id); } } + SendMessage { ref message } => { + goria + .write::() + .chat + .add_message(message.clone()); + } } } } diff --git a/egregoria/src/init.rs b/egregoria/src/init.rs index 56c6dbf4..968132f9 100644 --- a/egregoria/src/init.rs +++ b/egregoria/src/init.rs @@ -4,6 +4,7 @@ use crate::map_dynamic::{ dispatch_system, itinerary_update, routing_changed_system, routing_update_system, BuildingInfos, Dispatcher, ParkingManagement, }; +use crate::multiplayer::MultiplayerState; use crate::physics::coworld_synchronize; use crate::souls::freight_station::freight_station_system; use crate::souls::goods_company::{company_system, GoodsCompanyRegistry}; @@ -60,6 +61,7 @@ pub fn init() { register_init(init_market); + register_resource_default::("multiplayer_state"); register_resource_default::("random_vehicles"); register_resource_default::("tick"); register_resource_default::("map"); diff --git a/egregoria/src/lib.rs b/egregoria/src/lib.rs index ebc2e928..1c224a06 100644 --- a/egregoria/src/lib.rs +++ b/egregoria/src/lib.rs @@ -40,6 +40,7 @@ pub mod engine_interaction; pub mod init; pub mod map; pub mod map_dynamic; +pub mod multiplayer; pub mod physics; pub mod souls; #[cfg(test)] diff --git a/egregoria/src/multiplayer/chat.rs b/egregoria/src/multiplayer/chat.rs new file mode 100644 index 00000000..e8fae1c6 --- /dev/null +++ b/egregoria/src/multiplayer/chat.rs @@ -0,0 +1,44 @@ +use crate::utils::time::{GameInstant, GameTime}; +use geom::Color; +use serde::{Deserialize, Serialize}; + +#[derive(Default, Serialize, Deserialize)] +pub struct Chat { + pub messages: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message { + pub name: String, + pub text: String, + pub sent_at: GameInstant, + pub color: Color, + pub kind: MessageKind, +} + +#[derive(Debug, Copy, Clone, Serialize, Deserialize)] +pub enum MessageKind { + Info, + Warning, + PlayerChat, +} + +impl Chat { + pub fn add_message(&mut self, message: Message) { + self.messages.push(message); + } + + pub fn messages_since(&self, time: GameInstant) -> impl Iterator + '_ { + self.messages + .iter() + .filter(move |m| m.sent_at >= time) + .rev() + } +} + +impl Message { + /// Returns the number of (game) seconds elapsed since the message was sent + pub fn age_secs(&self, now: &GameTime) -> f64 { + self.sent_at.elapsed(now) + } +} diff --git a/egregoria/src/multiplayer/mod.rs b/egregoria/src/multiplayer/mod.rs new file mode 100644 index 00000000..7616feb2 --- /dev/null +++ b/egregoria/src/multiplayer/mod.rs @@ -0,0 +1,9 @@ +use crate::multiplayer::chat::Chat; +use serde::{Deserialize, Serialize}; + +pub mod chat; + +#[derive(Default, Serialize, Deserialize)] +pub struct MultiplayerState { + pub chat: Chat, +} diff --git a/egregoria/src/utils/time.rs b/egregoria/src/utils/time.rs index f501cdf5..8e740d99 100644 --- a/egregoria/src/utils/time.rs +++ b/egregoria/src/utils/time.rs @@ -14,7 +14,7 @@ pub const TICKS_PER_SECOND: u32 = 50; pub struct Tick(pub u32); /// An in-game instant used to measure time differences -#[derive(Inspect, Debug, Copy, Clone, Serialize, Deserialize)] +#[derive(Inspect, PartialEq, PartialOrd, Debug, Copy, Clone, Serialize, Deserialize)] pub struct GameInstant { /// Time in seconds elapsed since the start of the game pub timestamp: f64, diff --git a/native_app/src/gui/chat.rs b/native_app/src/gui/chat.rs new file mode 100644 index 00000000..49a4889f --- /dev/null +++ b/native_app/src/gui/chat.rs @@ -0,0 +1,127 @@ +use egui::panel::TopBottomSide; +use egui::{Align2, Color32, Frame, RichText, ScrollArea, TextBuffer, TopBottomPanel}; + +use egregoria::engine_interaction::WorldCommand; +use egregoria::multiplayer::chat::{Message, MessageKind}; +use egregoria::multiplayer::MultiplayerState; +use egregoria::utils::time::{GameInstant, GameTime, SECONDS_PER_REALTIME_SECOND}; +use egregoria::Egregoria; +use geom::Color; + +use crate::inputmap::{InputAction, InputMap}; +use crate::uiworld::UiWorld; + +#[derive(Default)] +pub struct GUIChatState { + cur_msg: String, + chat_bar_showed: bool, +} + +pub fn chat(ui: &egui::Context, uiw: &mut UiWorld, goria: &Egregoria) { + const MAX_MESSAGES: usize = 30; + let mut state = uiw.write::(); + let one_minute_ago = GameInstant { + timestamp: goria.read::().instant().timestamp + - 120.0 * SECONDS_PER_REALTIME_SECOND as f64, + }; + + let mstate = goria.read::(); + + let just_opened = uiw + .read::() + .just_act + .contains(&InputAction::OpenChat); + + if just_opened { + state.chat_bar_showed = true; + } + + let msgs: Vec<_> = mstate + .chat + .messages_since(one_minute_ago) + .take(MAX_MESSAGES) + .collect(); + + if !state.chat_bar_showed && msgs.len() == 0 { + return; + } + + egui::Window::new("Chat") + .title_bar(false) + .fixed_size(egui::Vec2::new(250.0, 300.0)) + .frame(Frame::default().fill(if state.chat_bar_showed { + Color32::from_black_alpha(192) + } else { + Color32::from_black_alpha(64) + })) + .anchor(Align2::LEFT_BOTTOM, (0.0, -55.0)) + .show(ui, |ui| { + ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| { + ui.allocate_space(egui::Vec2::new(250.0, 0.0)); + + if msgs.len() < 12 { + ui.add_space((12 - msgs.len()) as f32 * tweak!(24.0)); + } + + for message in msgs.iter().rev() { + let color = message.color; + + let text = RichText::new(message.text.clone()); + + ui.horizontal_wrapped(|ui| { + ui.add_space(5.0); + ui.colored_label( + Color32::from_rgb( + (color.r * 255.0) as u8, + (color.g * 255.0) as u8, + (color.b * 255.0) as u8, + ), + text, + ); + }); + ui.add_space(2.0); + } + }); + + TopBottomPanel::new(TopBottomSide::Bottom, "chat_bar") + .frame(Frame::default()) + .show_separator_line(false) + .show_inside(ui, |ui| { + if state.chat_bar_showed { + let response = ui.add( + egui::TextEdit::singleline(&mut state.cur_msg) + .desired_width(250.0) + .margin(egui::Vec2::new(8.0, 6.0)), + ); + + if just_opened { + response.request_focus(); + } + + if response.lost_focus() { + let msg = state.cur_msg.take(); + + if msg.len() > 0 && ui.input(|i| i.key_pressed(egui::Key::Enter)) { + let rng = common::rand::randu64(common::hash_u64(msg.as_bytes())); + + let color = Color::hsv(rng * 360.0, 0.8, 1.0, 1.0); + + uiw.commands().push(WorldCommand::SendMessage { + message: Message { + name: "player".to_string(), + text: msg, + sent_at: goria.read::().instant(), + color, + kind: MessageKind::PlayerChat, + }, + }) + } + + state.chat_bar_showed = false; + } + } else { + ui.allocate_space(egui::Vec2::new(240.0, 26.0)); + } + }); + }); +} diff --git a/native_app/src/gui/mod.rs b/native_app/src/gui/mod.rs index 852b9e52..b6adbd6d 100644 --- a/native_app/src/gui/mod.rs +++ b/native_app/src/gui/mod.rs @@ -13,6 +13,7 @@ use roadbuild::RoadBuildResource; pub mod addtrain; pub mod bulldozer; +pub mod chat; pub mod follow; pub mod inspect; pub mod inspected_aura; diff --git a/native_app/src/gui/topgui.rs b/native_app/src/gui/topgui.rs index d837657a..178e26e5 100644 --- a/native_app/src/gui/topgui.rs +++ b/native_app/src/gui/topgui.rs @@ -1,4 +1,5 @@ use crate::gui::bulldozer::BulldozerState; +use crate::gui::chat::chat; use crate::gui::inspect::inspector; use crate::gui::lotbrush::LotBrushResource; use crate::gui::roadeditor::RoadEditorResource; @@ -75,6 +76,8 @@ impl Gui { inspector(ui, uiworld, goria); + chat(ui, uiworld, goria); + self.windows.render(ui, uiworld, goria); Self::toolbox(ui, uiworld, goria); diff --git a/native_app/src/init.rs b/native_app/src/init.rs index 54f25cb2..aa6431bd 100644 --- a/native_app/src/init.rs +++ b/native_app/src/init.rs @@ -1,5 +1,6 @@ use crate::game_loop::Timings; use crate::gui::bulldozer::BulldozerState; +use crate::gui::chat::GUIChatState; use crate::gui::lotbrush::LotBrushResource; use crate::gui::roadbuild::RoadBuildResource; use crate::gui::roadeditor::RoadEditorResource; @@ -38,6 +39,7 @@ pub fn init() { register_resource_noserialize::(); register_resource_noserialize::(); register_resource_noserialize::(); + register_resource_noserialize::(); register_resource_noserialize::(); register_resource_noserialize::(); register_resource_noserialize::(); diff --git a/native_app/src/inputmap.rs b/native_app/src/inputmap.rs index 122f3cf2..a0c861e5 100644 --- a/native_app/src/inputmap.rs +++ b/native_app/src/inputmap.rs @@ -38,6 +38,7 @@ pub enum InputAction { DownElevation, OpenEconomyMenu, PausePlay, + OpenChat, } // All unit inputs need to match @@ -88,6 +89,7 @@ const DEFAULT_BINDINGS: &[(InputAction, &[&[UnitInput]])] = &[ (DownElevation, &[&[Key(K::LControl), WheelDown]]), (OpenEconomyMenu, &[&[Key(K::E)]]), (PausePlay, &[&[Key(K::Space)]]), + (OpenChat, &[&[Key(K::T)]]), ]; impl Default for Bindings { @@ -303,23 +305,24 @@ impl Display for InputAction { f, "{}", match self { - InputAction::GoLeft => "Go Left", - InputAction::GoRight => "Go Right", - InputAction::GoForward => "Go Forward", - InputAction::GoBackward => "Go Backward", - InputAction::CameraMove => "Camera Move", - InputAction::CameraRotate => "Camera Rotate", - InputAction::Zoom => "Zoom", - InputAction::Dezoom => "Dezoom", - InputAction::Rotate => "Rotate", - InputAction::Close => "Close", - InputAction::Select => "Select", - InputAction::HideInterface => "Hide interface", - InputAction::NoSnapping => "No Snapping", - InputAction::UpElevation => "Up Elevation", - InputAction::DownElevation => "Down Elevation", - InputAction::OpenEconomyMenu => "Economy Menu", - InputAction::PausePlay => "Pause/Play", + GoLeft => "Go Left", + GoRight => "Go Right", + GoForward => "Go Forward", + GoBackward => "Go Backward", + CameraMove => "Camera Move", + CameraRotate => "Camera Rotate", + Zoom => "Zoom", + Dezoom => "Dezoom", + Rotate => "Rotate", + Close => "Close", + Select => "Select", + HideInterface => "Hide interface", + NoSnapping => "No Snapping", + UpElevation => "Up Elevation", + DownElevation => "Down Elevation", + OpenEconomyMenu => "Economy Menu", + PausePlay => "Pause/Play", + OpenChat => "Interact with Chat", } ) }