Merge branch 'CapsizeGlimmer/chat_modes' into 'master'

Capsize glimmer/chat modes

Closes #217

See merge request veloren/veloren!1043
This commit is contained in:
Forest Anderson 2020-06-28 14:35:39 +00:00
commit 9246e34c1b
60 changed files with 1681 additions and 681 deletions

View File

@ -17,7 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Eyebrows and shapes can now be selected - Eyebrows and shapes can now be selected
- Character name and level information to chat, social tab and `/players` command - Character name and level information to chat, social tab and `/players` command
- Added inventory, armour and weapon saving - Added inventory, armour and weapon saving
- Show where screenshots are saved to in the chat - Show where screenshots are saved in the chat
- Added basic auto walk - Added basic auto walk
- Added weapon/attack sound effects - Added weapon/attack sound effects
- M2 attack for bow - M2 attack for bow
@ -26,6 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Server whitelist - Server whitelist
- Optional server-side maximum view distance - Optional server-side maximum view distance
- MOTD on login - MOTD on login
- Added group chat `/join_group` `/group`
- Added faction chat `/join_faction` `/faction`
- Added regional, local, and global chat (`/region`, `/say`, and `/world`, respectively)
- Added command shortcuts for each of the above chat modes (`/g`, `/f`, `/r`, `/s`, and `/w`, respectively and `/t` for `/tell`)
### Changed ### Changed

BIN
assets/voxygen/element/frames/bubble_dark/icon_frame.png (Stored with Git LFS) Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/command_error_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/command_info_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/faction.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/faction_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/group.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/group_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/kill_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/offline_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/online_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/private_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/region.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/region_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/say.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/say_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/tell.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/tell_small.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/world.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
assets/voxygen/element/icons/chat/world_small.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -252,12 +252,15 @@ magically infused items?"#,
"hud.settings.cumulated_damage": "Cumulated Damage", "hud.settings.cumulated_damage": "Cumulated Damage",
"hud.settings.incoming_damage": "Incoming Damage", "hud.settings.incoming_damage": "Incoming Damage",
"hud.settings.cumulated_incoming_damage": "Cumulated Incoming Damage", "hud.settings.cumulated_incoming_damage": "Cumulated Incoming Damage",
"hud.settings.speech_bubble": "Speech Bubble",
"hud.settings.speech_bubble_dark_mode": "Speech Bubble Dark Mode", "hud.settings.speech_bubble_dark_mode": "Speech Bubble Dark Mode",
"hud.settings.speech_bubble_icon": "Speech Bubble Icon",
"hud.settings.energybar_numbers": "Energybar Numbers", "hud.settings.energybar_numbers": "Energybar Numbers",
"hud.settings.values": "Values", "hud.settings.values": "Values",
"hud.settings.percentages": "Percentages", "hud.settings.percentages": "Percentages",
"hud.settings.chat": "Chat", "hud.settings.chat": "Chat",
"hud.settings.background_transparency": "Background Transparency", "hud.settings.background_transparency": "Background Transparency",
"hud.settings.chat_character_name": "Show Character Names",
"hud.settings.pan_sensitivity": "Pan Sensitivity", "hud.settings.pan_sensitivity": "Pan Sensitivity",
"hud.settings.zoom_sensitivity": "Zoom Sensitivity", "hud.settings.zoom_sensitivity": "Zoom Sensitivity",

View File

@ -82,9 +82,10 @@ fn main() {
}, },
}; };
const SHOW_NAME: bool = false;
for event in events { for event in events {
match event { match event {
Event::Chat { message, .. } => println!("{}", message), Event::Chat(m) => println!("{}", client.format_message(&m, SHOW_NAME)),
Event::Disconnect => {}, // TODO Event::Disconnect => {}, // TODO
Event::DisconnectionNotification(time) => { Event::DisconnectionNotification(time) => {
let message = match time { let message = match time {

View File

@ -25,7 +25,7 @@ impl TabComplete for ArgumentSpec {
}, },
ArgumentSpec::Any(_, _) => vec![], ArgumentSpec::Any(_, _) => vec![],
ArgumentSpec::Command(_) => complete_command(part), ArgumentSpec::Command(_) => complete_command(part),
ArgumentSpec::Message => complete_player(part, &client), ArgumentSpec::Message(_) => complete_player(part, &client),
ArgumentSpec::SubCommand => complete_command(part), ArgumentSpec::SubCommand => complete_command(part),
ArgumentSpec::Enum(_, strings, _) => strings ArgumentSpec::Enum(_, strings, _) => strings
.iter() .iter()
@ -47,9 +47,10 @@ fn complete_player(part: &str, client: &Client) -> Vec<String> {
} }
fn complete_command(part: &str) -> Vec<String> { fn complete_command(part: &str) -> Vec<String> {
CHAT_COMMANDS CHAT_SHORTCUTS
.iter() .keys()
.map(|com| com.keyword()) .map(ToString::to_string)
.chain(CHAT_COMMANDS.iter().map(ToString::to_string))
.filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part)) .filter(|kwd| kwd.starts_with(part) || format!("/{}", kwd).starts_with(part))
.map(|c| format!("/{}", c)) .map(|c| format!("/{}", c))
.collect() .collect()
@ -78,14 +79,13 @@ fn nth_word(line: &str, n: usize) -> Option<usize> {
None None
} }
#[allow(clippy::chars_next_cmp)] // TODO: Pending review in #587
pub fn complete(line: &str, client: &Client) -> Vec<String> { pub fn complete(line: &str, client: &Client) -> Vec<String> {
let word = if line.chars().last().map_or(true, char::is_whitespace) { let word = if line.chars().last().map_or(true, char::is_whitespace) {
"" ""
} else { } else {
line.split_whitespace().last().unwrap_or("") line.split_whitespace().last().unwrap_or("")
}; };
if line.chars().next() == Some('/') { if line.starts_with('/') {
let mut iter = line.split_whitespace(); let mut iter = line.split_whitespace();
let cmd = iter.next().unwrap(); let cmd = iter.next().unwrap();
let i = iter.count() + if word.is_empty() { 1 } else { 0 }; let i = iter.count() + if word.is_empty() { 1 } else { 0 };
@ -106,7 +106,7 @@ pub fn complete(line: &str, client: &Client) -> Vec<String> {
vec![] vec![]
} }
}, },
Some(ArgumentSpec::Message) => complete_player(word, &client), Some(ArgumentSpec::Message(_)) => complete_player(word, &client),
_ => vec![], // End of command. Nothing to complete _ => vec![], // End of command. Nothing to complete
} }
} }

View File

@ -32,7 +32,6 @@ use common::{
sync::{Uid, UidAllocator, WorldSyncExt}, sync::{Uid, UidAllocator, WorldSyncExt},
terrain::{block::Block, TerrainChunk, TerrainChunkSize}, terrain::{block::Block, TerrainChunk, TerrainChunkSize},
vol::RectVolSize, vol::RectVolSize,
ChatType,
}; };
use hashbrown::HashMap; use hashbrown::HashMap;
use image::DynamicImage; use image::DynamicImage;
@ -55,10 +54,7 @@ const SERVER_TIMEOUT: f64 = 20.0;
const SERVER_TIMEOUT_GRACE_PERIOD: f64 = 14.0; const SERVER_TIMEOUT_GRACE_PERIOD: f64 = 14.0;
pub enum Event { pub enum Event {
Chat { Chat(comp::ChatMsg),
chat_type: ChatType,
message: String,
},
Disconnect, Disconnect,
DisconnectionNotification(u64), DisconnectionNotification(u64),
Notification(Notification), Notification(Notification),
@ -70,7 +66,7 @@ pub struct Client {
thread_pool: ThreadPool, thread_pool: ThreadPool,
pub server_info: ServerInfo, pub server_info: ServerInfo,
pub world_map: (Arc<DynamicImage>, Vec2<u32>), pub world_map: (Arc<DynamicImage>, Vec2<u32>),
pub player_list: HashMap<u64, PlayerInfo>, pub player_list: HashMap<Uid, PlayerInfo>,
pub character_list: CharacterList, pub character_list: CharacterList,
pub active_character_id: Option<i32>, pub active_character_id: Option<i32>,
@ -465,8 +461,8 @@ impl Client {
/// Send a chat message to the server. /// Send a chat message to the server.
pub fn send_chat(&mut self, message: String) { pub fn send_chat(&mut self, message: String) {
match validate_chat_msg(&message) { match validate_chat_msg(&message) {
Ok(()) => self.postbox.send_message(ClientMsg::ChatMsg { message }), Ok(()) => self.postbox.send_message(ClientMsg::ChatMsg(message)),
Err(ChatMsgValidationError::TooLong) => warn!( Err(ChatMsgValidationError::TooLong) => tracing::warn!(
"Attempted to send a message that's too long (Over {} bytes)", "Attempted to send a message that's too long (Over {} bytes)",
MAX_BYTES_CHAT_MSG MAX_BYTES_CHAT_MSG
), ),
@ -763,6 +759,17 @@ impl Client {
); );
} }
}, },
ServerMsg::PlayerListUpdate(PlayerListUpdate::Admin(uid, admin)) => {
if let Some(player_info) = self.player_list.get_mut(&uid) {
player_info.is_admin = admin;
} else {
warn!(
"Received msg to update admin status of uid {}, but they were not \
in the list.",
uid
);
}
},
ServerMsg::PlayerListUpdate(PlayerListUpdate::SelectedCharacter( ServerMsg::PlayerListUpdate(PlayerListUpdate::SelectedCharacter(
uid, uid,
char_info, char_info,
@ -797,7 +804,23 @@ impl Client {
} }
}, },
ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => { ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(uid)) => {
if self.player_list.remove(&uid).is_none() { // Instead of removing players, mark them as offline because we need to
// remember the names of disconnected players in chat.
//
// TODO the server should re-use uids of players that log out and log back
// in.
if let Some(player_info) = self.player_list.get_mut(&uid) {
if player_info.is_online {
player_info.is_online = false;
} else {
warn!(
"Received msg to remove uid {} from the player list by they \
were already marked offline",
uid
);
}
} else {
warn!( warn!(
"Received msg to remove uid {} from the player list by they \ "Received msg to remove uid {} from the player list by they \
weren't in the list!", weren't in the list!",
@ -824,11 +847,9 @@ impl Client {
self.last_ping_delta = self.last_ping_delta =
(self.state.get_time() - self.last_server_ping).round(); (self.state.get_time() - self.last_server_ping).round();
}, },
ServerMsg::ChatMsg { message, chat_type } => { ServerMsg::ChatMsg(m) => frontend_events.push(Event::Chat(m)),
frontend_events.push(Event::Chat { message, chat_type })
},
ServerMsg::SetPlayerEntity(uid) => { ServerMsg::SetPlayerEntity(uid) => {
if let Some(entity) = self.state.ecs().entity_from_uid(uid) { if let Some(entity) = self.state.ecs().entity_from_uid(uid.0) {
self.entity = entity; self.entity = entity;
} else { } else {
return Err(Error::Other("Failed to find entity from uid.".to_owned())); return Err(Error::Other("Failed to find entity from uid.".to_owned()));
@ -851,15 +872,10 @@ impl Client {
self.state.ecs_mut().apply_entity_package(entity_package); self.state.ecs_mut().apply_entity_package(entity_package);
}, },
ServerMsg::DeleteEntity(entity) => { ServerMsg::DeleteEntity(entity) => {
if self if self.state.read_component_cloned::<Uid>(self.entity) != Some(entity) {
.state
.read_component_cloned::<Uid>(self.entity)
.map(|u| u.into())
!= Some(entity)
{
self.state self.state
.ecs_mut() .ecs_mut()
.delete_entity_and_clear_from_uid_allocator(entity); .delete_entity_and_clear_from_uid_allocator(entity.0);
} }
}, },
// Cleanup for when the client goes back to the `Registered` state // Cleanup for when the client goes back to the `Registered` state
@ -876,12 +892,13 @@ impl Client {
match event { match event {
InventoryUpdateEvent::CollectFailed => { InventoryUpdateEvent::CollectFailed => {
frontend_events.push(Event::Chat { // TODO This might not be the best way to show an error
frontend_events.push(Event::Chat(comp::ChatMsg {
message: String::from( message: String::from(
"Failed to collect item. Your inventory may be full!", "Failed to collect item. Your inventory may be full!",
), ),
chat_type: ChatType::Meta, chat_type: comp::ChatType::CommandError,
}) }))
}, },
_ => { _ => {
if let InventoryUpdateEvent::Collected(item) = event { if let InventoryUpdateEvent::Collected(item) = event {
@ -1005,6 +1022,74 @@ impl Client {
self.entity = entity_builder.with(uid).build(); self.entity = entity_builder.with(uid).build();
} }
/// Format a message for the client (voxygen chat box or chat-cli)
pub fn format_message(&self, msg: &comp::ChatMsg, character_name: bool) -> String {
let comp::ChatMsg { chat_type, message } = &msg;
let alias_of_uid = |uid| {
self.player_list
.get(uid)
.map_or("<?>".to_string(), |player_info| {
if player_info.is_admin {
format!("ADMIN - {}", player_info.player_alias)
} else {
player_info.player_alias.to_string()
}
})
};
let name_of_uid = |uid| {
let ecs = self.state.ecs();
(
&ecs.read_storage::<comp::Stats>(),
&ecs.read_storage::<Uid>(),
)
.join()
.find(|(_, u)| u == &uid)
.map(|(c, _)| c.name.clone())
};
let message_format = |uid, message, group| {
let alias = alias_of_uid(uid);
let name = if character_name {
name_of_uid(uid)
} else {
None
};
match (group, name) {
(Some(group), None) => format!("({}) [{}]: {}", group, alias, message),
(None, None) => format!("[{}]: {}", alias, message),
(Some(group), Some(name)) => {
format!("({}) [{}] {}: {}", group, alias, name, message)
},
(None, Some(name)) => format!("[{}] {}: {}", alias, name, message),
}
};
match chat_type {
comp::ChatType::Online => message.to_string(),
comp::ChatType::Offline => message.to_string(),
comp::ChatType::CommandError => message.to_string(),
comp::ChatType::CommandInfo => message.to_string(),
comp::ChatType::FactionMeta(_) => message.to_string(),
comp::ChatType::GroupMeta(_) => message.to_string(),
comp::ChatType::Kill => message.to_string(),
comp::ChatType::Tell(from, to) => {
let from_alias = alias_of_uid(from);
let to_alias = alias_of_uid(to);
if Some(from) == self.state.ecs().read_storage::<Uid>().get(self.entity) {
format!("To [{}]: {}", to_alias, message)
} else {
format!("From [{}]: {}", from_alias, message)
}
},
comp::ChatType::Say(uid) => message_format(uid, message, None),
comp::ChatType::Group(uid, s) => message_format(uid, message, Some(s)),
comp::ChatType::Faction(uid, s) => message_format(uid, message, Some(s)),
comp::ChatType::Region(uid) => message_format(uid, message, None),
comp::ChatType::World(uid) => message_format(uid, message, None),
// NPCs can't talk. Should be filtered by hud/mod.rs for voxygen and should be filtered
// by server (due to not having a Pos) for chat-cli
comp::ChatType::Npc(_uid, _r) => "".to_string(),
}
}
} }
impl Drop for Client { impl Drop for Client {

View File

@ -1,6 +1,11 @@
use crate::{assets, comp, npc}; use crate::{assets, comp, npc};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::{ops::Deref, path::Path, str::FromStr}; use std::{
collections::HashMap,
fmt::{self, Display},
path::Path,
str::FromStr,
};
use tracing::warn; use tracing::warn;
/// Struct representing a command that a user can run from server chat. /// Struct representing a command that a user can run from server chat.
@ -9,13 +14,16 @@ pub struct ChatCommandData {
pub args: Vec<ArgumentSpec>, pub args: Vec<ArgumentSpec>,
/// A one-line message that explains what the command does /// A one-line message that explains what the command does
pub description: &'static str, pub description: &'static str,
/// A boolean that is used to check whether the command requires /// Whether the command requires administrator permissions.
/// administrator permissions or not. pub needs_admin: IsAdminOnly,
pub needs_admin: bool,
} }
impl ChatCommandData { impl ChatCommandData {
pub fn new(args: Vec<ArgumentSpec>, description: &'static str, needs_admin: bool) -> Self { pub fn new(
args: Vec<ArgumentSpec>,
description: &'static str,
needs_admin: IsAdminOnly,
) -> Self {
Self { Self {
args, args,
description, description,
@ -33,11 +41,15 @@ pub enum ChatCommand {
Debug, Debug,
DebugColumn, DebugColumn,
Explosion, Explosion,
Faction,
GiveExp, GiveExp,
GiveItem, GiveItem,
Goto, Goto,
Group,
Health, Health,
Help, Help,
JoinFaction,
JoinGroup,
Jump, Jump,
Kill, Kill,
KillNpcs, KillNpcs,
@ -46,7 +58,9 @@ pub enum ChatCommand {
Motd, Motd,
Object, Object,
Players, Players,
Region,
RemoveLights, RemoveLights,
Say,
SetLevel, SetLevel,
SetMotd, SetMotd,
Spawn, Spawn,
@ -56,6 +70,7 @@ pub enum ChatCommand {
Tp, Tp,
Version, Version,
Waypoint, Waypoint,
World,
} }
// Thank you for keeping this sorted alphabetically :-) // Thank you for keeping this sorted alphabetically :-)
@ -66,11 +81,15 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Debug, ChatCommand::Debug,
ChatCommand::DebugColumn, ChatCommand::DebugColumn,
ChatCommand::Explosion, ChatCommand::Explosion,
ChatCommand::Faction,
ChatCommand::GiveExp, ChatCommand::GiveExp,
ChatCommand::GiveItem, ChatCommand::GiveItem,
ChatCommand::Goto, ChatCommand::Goto,
ChatCommand::Group,
ChatCommand::Health, ChatCommand::Health,
ChatCommand::Help, ChatCommand::Help,
ChatCommand::JoinFaction,
ChatCommand::JoinGroup,
ChatCommand::Jump, ChatCommand::Jump,
ChatCommand::Kill, ChatCommand::Kill,
ChatCommand::KillNpcs, ChatCommand::KillNpcs,
@ -79,7 +98,9 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Motd, ChatCommand::Motd,
ChatCommand::Object, ChatCommand::Object,
ChatCommand::Players, ChatCommand::Players,
ChatCommand::Region,
ChatCommand::RemoveLights, ChatCommand::RemoveLights,
ChatCommand::Say,
ChatCommand::SetLevel, ChatCommand::SetLevel,
ChatCommand::SetMotd, ChatCommand::SetMotd,
ChatCommand::Spawn, ChatCommand::Spawn,
@ -89,9 +110,19 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Tp, ChatCommand::Tp,
ChatCommand::Version, ChatCommand::Version,
ChatCommand::Waypoint, ChatCommand::Waypoint,
ChatCommand::World,
]; ];
lazy_static! { lazy_static! {
pub static ref CHAT_SHORTCUTS: HashMap<char, ChatCommand> = [
('f', ChatCommand::Faction),
('g', ChatCommand::Group),
('r', ChatCommand::Region),
('s', ChatCommand::Say),
('t', ChatCommand::Tell),
('w', ChatCommand::World),
].iter().cloned().collect();
static ref ALIGNMENTS: Vec<String> = vec!["wild", "enemy", "npc", "pet"] static ref ALIGNMENTS: Vec<String> = vec!["wild", "enemy", "npc", "pet"]
.iter() .iter()
.map(|s| s.to_string()) .map(|s| s.to_string())
@ -141,31 +172,37 @@ lazy_static! {
impl ChatCommand { impl ChatCommand {
pub fn data(&self) -> ChatCommandData { pub fn data(&self) -> ChatCommandData {
use ArgumentSpec::*; use ArgumentSpec::*;
use IsAdminOnly::*;
use Requirement::*; use Requirement::*;
let cmd = ChatCommandData::new; let cmd = ChatCommandData::new;
match self { match self {
ChatCommand::Adminify => cmd( ChatCommand::Adminify => cmd(
vec![PlayerName(Required)], vec![PlayerName(Required)],
"Temporarily gives a player admin permissions or removes them", "Temporarily gives a player admin permissions or removes them",
true, Admin,
), ),
ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", false), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin),
ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", true), ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", Admin),
ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", true), ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", Admin),
ChatCommand::DebugColumn => cmd( ChatCommand::DebugColumn => cmd(
vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], vec![Integer("x", 15000, Required), Integer("y", 15000, Required)],
"Prints some debug information about a column", "Prints some debug information about a column",
false, NoAdmin,
), ),
ChatCommand::Explosion => cmd( ChatCommand::Explosion => cmd(
vec![Float("radius", 5.0, Required)], vec![Float("radius", 5.0, Required)],
"Explodes the ground around you", "Explodes the ground around you",
true, Admin,
),
ChatCommand::Faction => cmd(
vec![Message(Optional)],
"Send messages to your faction",
NoAdmin,
), ),
ChatCommand::GiveExp => cmd( ChatCommand::GiveExp => cmd(
vec![Integer("amount", 50, Required)], vec![Integer("amount", 50, Required)],
"Give experience to yourself", "Give experience to yourself",
true, Admin,
), ),
ChatCommand::GiveItem => cmd( ChatCommand::GiveItem => cmd(
vec![ vec![
@ -173,7 +210,7 @@ impl ChatCommand {
Integer("num", 1, Optional), Integer("num", 1, Optional),
], ],
"Give yourself some items", "Give yourself some items",
true, Admin,
), ),
ChatCommand::Goto => cmd( ChatCommand::Goto => cmd(
vec![ vec![
@ -182,17 +219,32 @@ impl ChatCommand {
Float("z", 0.0, Required), Float("z", 0.0, Required),
], ],
"Teleport to a position", "Teleport to a position",
true, Admin,
),
ChatCommand::Group => cmd(
vec![Message(Optional)],
"Send messages to your group",
NoAdmin,
), ),
ChatCommand::Health => cmd( ChatCommand::Health => cmd(
vec![Integer("hp", 100, Required)], vec![Integer("hp", 100, Required)],
"Set your current health", "Set your current health",
true, Admin,
), ),
ChatCommand::Help => ChatCommandData::new( ChatCommand::Help => ChatCommandData::new(
vec![Command(Optional)], vec![Command(Optional)],
"Display information about commands", "Display information about commands",
false, NoAdmin,
),
ChatCommand::JoinFaction => ChatCommandData::new(
vec![Any("faction", Optional)],
"Join/leave the specified faction",
NoAdmin,
),
ChatCommand::JoinGroup => ChatCommandData::new(
vec![Any("group", Optional)],
"Join/leave the specified group",
NoAdmin,
), ),
ChatCommand::Jump => cmd( ChatCommand::Jump => cmd(
vec![ vec![
@ -201,10 +253,10 @@ impl ChatCommand {
Float("z", 0.0, Required), Float("z", 0.0, Required),
], ],
"Offset your current position", "Offset your current position",
true, Admin,
), ),
ChatCommand::Kill => cmd(vec![], "Kill yourself", false), ChatCommand::Kill => cmd(vec![], "Kill yourself", NoAdmin),
ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", true), ChatCommand::KillNpcs => cmd(vec![], "Kill the NPCs", Admin),
ChatCommand::Lantern => cmd( ChatCommand::Lantern => cmd(
vec![ vec![
Float("strength", 5.0, Required), Float("strength", 5.0, Required),
@ -213,7 +265,7 @@ impl ChatCommand {
Float("b", 1.0, Optional), Float("b", 1.0, Optional),
], ],
"Change your lantern's strength and color", "Change your lantern's strength and color",
true, Admin,
), ),
ChatCommand::Light => cmd( ChatCommand::Light => cmd(
vec![ vec![
@ -226,26 +278,42 @@ impl ChatCommand {
Float("strength", 5.0, Optional), Float("strength", 5.0, Optional),
], ],
"Spawn entity with light", "Spawn entity with light",
true, Admin,
),
ChatCommand::Motd => cmd(
vec![Message(Optional)],
"View the server description",
NoAdmin,
), ),
ChatCommand::Motd => cmd(vec![Message], "View the server description", false),
ChatCommand::Object => cmd( ChatCommand::Object => cmd(
vec![Enum("object", OBJECTS.clone(), Required)], vec![Enum("object", OBJECTS.clone(), Required)],
"Spawn an object", "Spawn an object",
true, Admin,
), ),
ChatCommand::Players => cmd(vec![], "Lists players currently online", false), ChatCommand::Players => cmd(vec![], "Lists players currently online", NoAdmin),
ChatCommand::RemoveLights => cmd( ChatCommand::RemoveLights => cmd(
vec![Float("radius", 20.0, Optional)], vec![Float("radius", 20.0, Optional)],
"Removes all lights spawned by players", "Removes all lights spawned by players",
true, Admin,
),
ChatCommand::Region => cmd(
vec![Message(Optional)],
"Send messages to everyone in your region of the world",
NoAdmin,
),
ChatCommand::Say => cmd(
vec![Message(Optional)],
"Send messages to everyone within shouting distance",
NoAdmin,
), ),
ChatCommand::SetLevel => cmd( ChatCommand::SetLevel => cmd(
vec![Integer("level", 10, Required)], vec![Integer("level", 10, Required)],
"Set player Level", "Set player Level",
true, Admin,
), ),
ChatCommand::SetMotd => cmd(vec![Message], "Set the server description", true), ChatCommand::SetMotd => {
cmd(vec![Message(Optional)], "Set the server description", Admin)
},
ChatCommand::Spawn => cmd( ChatCommand::Spawn => cmd(
vec![ vec![
Enum("alignment", ALIGNMENTS.clone(), Required), Enum("alignment", ALIGNMENTS.clone(), Required),
@ -253,32 +321,37 @@ impl ChatCommand {
Integer("amount", 1, Optional), Integer("amount", 1, Optional),
], ],
"Spawn a test entity", "Spawn a test entity",
true, Admin,
), ),
ChatCommand::Sudo => cmd( ChatCommand::Sudo => cmd(
vec![PlayerName(Required), SubCommand], vec![PlayerName(Required), SubCommand],
"Run command as if you were another player", "Run command as if you were another player",
true, Admin,
), ),
ChatCommand::Tell => cmd( ChatCommand::Tell => cmd(
vec![PlayerName(Required), Message], vec![PlayerName(Required), Message(Optional)],
"Send a message to another player", "Send a message to another player",
false, NoAdmin,
), ),
ChatCommand::Time => cmd( ChatCommand::Time => cmd(
vec![Enum("time", TIMES.clone(), Optional)], vec![Enum("time", TIMES.clone(), Optional)],
"Set the time of day", "Set the time of day",
true, Admin,
), ),
ChatCommand::Tp => cmd( ChatCommand::Tp => cmd(
vec![PlayerName(Optional)], vec![PlayerName(Optional)],
"Teleport to another player", "Teleport to another player",
true, Admin,
), ),
ChatCommand::Version => cmd(vec![], "Prints server version", false), ChatCommand::Version => cmd(vec![], "Prints server version", NoAdmin),
ChatCommand::Waypoint => { ChatCommand::Waypoint => {
cmd(vec![], "Set your waypoint to your current position", true) cmd(vec![], "Set your waypoint to your current position", Admin)
}, },
ChatCommand::World => cmd(
vec![Message(Optional)],
"Send messages to everyone on the server",
NoAdmin,
),
} }
} }
@ -291,10 +364,14 @@ impl ChatCommand {
ChatCommand::Debug => "debug", ChatCommand::Debug => "debug",
ChatCommand::DebugColumn => "debug_column", ChatCommand::DebugColumn => "debug_column",
ChatCommand::Explosion => "explosion", ChatCommand::Explosion => "explosion",
ChatCommand::Faction => "faction",
ChatCommand::GiveExp => "give_exp", ChatCommand::GiveExp => "give_exp",
ChatCommand::GiveItem => "give_item", ChatCommand::GiveItem => "give_item",
ChatCommand::Goto => "goto", ChatCommand::Goto => "goto",
ChatCommand::Group => "group",
ChatCommand::Health => "health", ChatCommand::Health => "health",
ChatCommand::JoinFaction => "join_faction",
ChatCommand::JoinGroup => "join_group",
ChatCommand::Help => "help", ChatCommand::Help => "help",
ChatCommand::Jump => "jump", ChatCommand::Jump => "jump",
ChatCommand::Kill => "kill", ChatCommand::Kill => "kill",
@ -304,7 +381,9 @@ impl ChatCommand {
ChatCommand::Motd => "motd", ChatCommand::Motd => "motd",
ChatCommand::Object => "object", ChatCommand::Object => "object",
ChatCommand::Players => "players", ChatCommand::Players => "players",
ChatCommand::Region => "region",
ChatCommand::RemoveLights => "remove_lights", ChatCommand::RemoveLights => "remove_lights",
ChatCommand::Say => "say",
ChatCommand::SetLevel => "set_level", ChatCommand::SetLevel => "set_level",
ChatCommand::SetMotd => "set_motd", ChatCommand::SetMotd => "set_motd",
ChatCommand::Spawn => "spawn", ChatCommand::Spawn => "spawn",
@ -314,6 +393,7 @@ impl ChatCommand {
ChatCommand::Tp => "tp", ChatCommand::Tp => "tp",
ChatCommand::Version => "version", ChatCommand::Version => "version",
ChatCommand::Waypoint => "waypoint", ChatCommand::Waypoint => "waypoint",
ChatCommand::World => "world",
} }
} }
@ -329,7 +409,7 @@ impl ChatCommand {
/// A boolean that is used to check whether the command requires /// A boolean that is used to check whether the command requires
/// administrator permissions or not. /// administrator permissions or not.
pub fn needs_admin(&self) -> bool { self.data().needs_admin } pub fn needs_admin(&self) -> bool { IsAdminOnly::Admin == self.data().needs_admin }
/// Returns a format string for parsing arguments with scan_fmt /// Returns a format string for parsing arguments with scan_fmt
pub fn arg_fmt(&self) -> String { pub fn arg_fmt(&self) -> String {
@ -342,15 +422,21 @@ impl ChatCommand {
ArgumentSpec::Integer(_, _, _) => "{d}", ArgumentSpec::Integer(_, _, _) => "{d}",
ArgumentSpec::Any(_, _) => "{}", ArgumentSpec::Any(_, _) => "{}",
ArgumentSpec::Command(_) => "{}", ArgumentSpec::Command(_) => "{}",
ArgumentSpec::Message => "{/.*/}", ArgumentSpec::Message(_) => "{/.*/}",
ArgumentSpec::SubCommand => "{} {/.*/}", ArgumentSpec::SubCommand => "{} {/.*/}",
ArgumentSpec::Enum(_, _, _) => "{}", // TODO ArgumentSpec::Enum(_, _, _) => "{}",
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
} }
} }
impl Display for ChatCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "{}", self.keyword())
}
}
impl FromStr for ChatCommand { impl FromStr for ChatCommand {
type Err = (); type Err = ();
@ -360,29 +446,37 @@ impl FromStr for ChatCommand {
} else { } else {
&keyword[..] &keyword[..]
}; };
for c in CHAT_COMMANDS { if keyword.len() == 1 {
if kwd == c.keyword() { if let Some(c) = keyword
.chars()
.next()
.as_ref()
.and_then(|k| CHAT_SHORTCUTS.get(k))
{
return Ok(*c); return Ok(*c);
} }
} else {
for c in CHAT_COMMANDS {
if kwd == c.keyword() {
return Ok(*c);
}
}
} }
Err(()) Err(())
} }
} }
#[derive(Eq, PartialEq, Debug)]
pub enum IsAdminOnly {
Admin,
NoAdmin,
}
#[derive(Eq, PartialEq, Debug)]
pub enum Requirement { pub enum Requirement {
Required, Required,
Optional, Optional,
} }
impl Deref for Requirement {
type Target = bool;
fn deref(&self) -> &bool {
match self {
Requirement::Required => &true,
Requirement::Optional => &false,
}
}
}
/// Representation for chat command arguments /// Representation for chat command arguments
pub enum ArgumentSpec { pub enum ArgumentSpec {
@ -404,7 +498,7 @@ pub enum ArgumentSpec {
Command(Requirement), Command(Requirement),
/// This is the final argument, consuming all characters until the end of /// This is the final argument, consuming all characters until the end of
/// input. /// input.
Message, Message(Requirement),
/// This command is followed by another command (such as in /sudo) /// This command is followed by another command (such as in /sudo)
SubCommand, SubCommand,
/// The argument is likely an enum. The associated values are /// The argument is likely an enum. The associated values are
@ -418,44 +512,50 @@ impl ArgumentSpec {
pub fn usage_string(&self) -> String { pub fn usage_string(&self) -> String {
match self { match self {
ArgumentSpec::PlayerName(req) => { ArgumentSpec::PlayerName(req) => {
if **req { if &Requirement::Required == req {
"<player>".to_string() "<player>".to_string()
} else { } else {
"[player]".to_string() "[player]".to_string()
} }
}, },
ArgumentSpec::Float(label, _, req) => { ArgumentSpec::Float(label, _, req) => {
if **req { if &Requirement::Required == req {
format!("<{}>", label) format!("<{}>", label)
} else { } else {
format!("[{}]", label) format!("[{}]", label)
} }
}, },
ArgumentSpec::Integer(label, _, req) => { ArgumentSpec::Integer(label, _, req) => {
if **req { if &Requirement::Required == req {
format!("<{}>", label) format!("<{}>", label)
} else { } else {
format!("[{}]", label) format!("[{}]", label)
} }
}, },
ArgumentSpec::Any(label, req) => { ArgumentSpec::Any(label, req) => {
if **req { if &Requirement::Required == req {
format!("<{}>", label) format!("<{}>", label)
} else { } else {
format!("[{}]", label) format!("[{}]", label)
} }
}, },
ArgumentSpec::Command(req) => { ArgumentSpec::Command(req) => {
if **req { if &Requirement::Required == req {
"<[/]command>".to_string() "<[/]command>".to_string()
} else { } else {
"[[/]command]".to_string() "[[/]command]".to_string()
} }
}, },
ArgumentSpec::Message => "<message>".to_string(), ArgumentSpec::Message(req) => {
if &Requirement::Required == req {
"<message>".to_string()
} else {
"[message]".to_string()
}
},
ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(), ArgumentSpec::SubCommand => "<[/]command> [args...]".to_string(),
ArgumentSpec::Enum(label, _, req) => { ArgumentSpec::Enum(label, _, req) => {
if **req { if &Requirement::Required == req {
format! {"<{}>", label} format! {"<{}>", label}
} else { } else {
format! {"[{}]", label} format! {"[{}]", label}

View File

@ -1,4 +1,5 @@
use specs::{Component, NullStorage}; use specs::{Component, NullStorage};
use std::ops::Deref;
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Default)]
pub struct Admin; pub struct Admin;
@ -6,3 +7,12 @@ pub struct Admin;
impl Component for Admin { impl Component for Admin {
type Storage = NullStorage<Self>; type Storage = NullStorage<Self>;
} }
/// List of admin usernames. This is stored as a specs resource so that the list
/// can be read by specs systems.
pub struct AdminList(pub Vec<String>);
impl Deref for AdminList {
type Target = Vec<String>;
fn deref(&self) -> &Vec<String> { &self.0 }
}

View File

@ -1,5 +1,5 @@
use crate::{path::Chaser, state::Time}; use crate::path::Chaser;
use specs::{Component, Entity as EcsEntity, FlaggedStorage, HashMapStorage}; use specs::{Component, Entity as EcsEntity};
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
use vek::*; use vek::*;
@ -107,50 +107,3 @@ impl Activity {
impl Default for Activity { impl Default for Activity {
fn default() -> Self { Activity::Idle(Vec2::zero()) } fn default() -> Self { Activity::Idle(Vec2::zero()) }
} }
/// Default duration in seconds of speech bubbles
pub const SPEECH_BUBBLE_DURATION: f64 = 5.0;
/// The contents of a speech bubble
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SpeechBubbleMessage {
/// This message was said by a player and needs no translation
Plain(String),
/// This message was said by an NPC. The fields are a i18n key and a random
/// u16 index
Localized(String, u16),
}
/// Adds a speech bubble to the entity
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SpeechBubble {
pub message: SpeechBubbleMessage,
pub timeout: Option<Time>,
// TODO add icon enum for player chat type / npc quest+trade
}
impl Component for SpeechBubble {
type Storage = FlaggedStorage<Self, HashMapStorage<Self>>;
}
impl SpeechBubble {
pub fn npc_new(i18n_key: String, now: Time) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key, rand::random());
let timeout = Some(Time(now.0 + SPEECH_BUBBLE_DURATION));
Self { message, timeout }
}
pub fn player_new(message: String, now: Time) -> Self {
let message = SpeechBubbleMessage::Plain(message);
let timeout = Some(Time(now.0 + SPEECH_BUBBLE_DURATION));
Self { message, timeout }
}
pub fn message<F>(&self, i18n_variation: F) -> String
where
F: Fn(String, u16) -> String,
{
match &self.message {
SpeechBubbleMessage::Plain(m) => m.to_string(),
SpeechBubbleMessage::Localized(k, i) => i18n_variation(k.to_string(), *i),
}
}
}

260
common/src/comp/chat.rs Normal file
View File

@ -0,0 +1,260 @@
use crate::{msg::ServerMsg, sync::Uid};
use specs::Component;
use specs_idvs::IDVStorage;
use std::time::{Duration, Instant};
/// A player's current chat mode. These are chat types that can only be sent by
/// the player.
#[derive(Clone, Debug)]
pub enum ChatMode {
/// Private message to another player (by uuid)
Tell(Uid),
/// Talk to players within shouting distance
Say,
/// Talk to players in your region of the world
Region,
/// Talk to your current group of players
Group(String),
/// Talk to your faction
Faction(String),
/// Talk to every player on the server
World,
}
impl Component for ChatMode {
type Storage = IDVStorage<Self>;
}
impl ChatMode {
/// Create a message from your current chat mode and uuid.
pub fn new_message(&self, from: Uid, message: String) -> ChatMsg {
let chat_type = match self {
ChatMode::Tell(to) => ChatType::Tell(from, *to),
ChatMode::Say => ChatType::Say(from),
ChatMode::Region => ChatType::Region(from),
ChatMode::Group(name) => ChatType::Group(from, name.to_string()),
ChatMode::Faction(name) => ChatType::Faction(from, name.to_string()),
ChatMode::World => ChatType::World(from),
};
ChatMsg { chat_type, message }
}
}
impl Default for ChatMode {
fn default() -> Self { Self::World }
}
/// List of chat types. Each one is colored differently and has its own icon.
///
/// This is a superset of `SpeechBubbleType`, which is a superset of `ChatMode`
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChatType {
/// A player came online
Online,
/// A player went offline
Offline,
/// The result of chat commands
CommandInfo,
/// A chat command failed
CommandError,
/// Inform players that someone died
Kill,
/// Server notifications to a group, such as player join/leave
GroupMeta(String),
/// Server notifications to a faction, such as player join/leave
FactionMeta(String),
/// One-on-one chat (from, to)
Tell(Uid, Uid),
/// Chat with nearby players
Say(Uid),
/// Group chat
Group(Uid, String),
/// Factional chat
Faction(Uid, String),
/// Regional chat
Region(Uid),
/// World chat
World(Uid),
/// Messages sent from NPCs (Not shown in chat but as speech bubbles)
///
/// The u16 field is a random number for selecting localization variants.
Npc(Uid, u16),
}
impl ChatType {
pub fn chat_msg<S>(self, msg: S) -> ChatMsg
where
S: Into<String>,
{
ChatMsg {
chat_type: self,
message: msg.into(),
}
}
pub fn server_msg<S>(self, msg: S) -> ServerMsg
where
S: Into<String>,
{
ServerMsg::ChatMsg(self.chat_msg(msg))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChatMsg {
pub chat_type: ChatType,
pub message: String,
}
impl ChatMsg {
pub const NPC_DISTANCE: f32 = 100.0;
pub const REGION_DISTANCE: f32 = 1000.0;
pub const SAY_DISTANCE: f32 = 100.0;
pub fn npc(uid: Uid, message: String) -> Self {
let chat_type = ChatType::Npc(uid, rand::random());
Self { chat_type, message }
}
pub fn to_bubble(&self) -> Option<(SpeechBubble, Uid)> {
let icon = self.icon();
if let ChatType::Npc(from, r) = self.chat_type {
Some((SpeechBubble::npc_new(&self.message, r, icon), from))
} else {
self.uid()
.map(|from| (SpeechBubble::player_new(&self.message, icon), from))
}
}
pub fn icon(&self) -> SpeechBubbleType {
match &self.chat_type {
ChatType::Online => SpeechBubbleType::None,
ChatType::Offline => SpeechBubbleType::None,
ChatType::CommandInfo => SpeechBubbleType::None,
ChatType::CommandError => SpeechBubbleType::None,
ChatType::FactionMeta(_) => SpeechBubbleType::None,
ChatType::GroupMeta(_) => SpeechBubbleType::None,
ChatType::Kill => SpeechBubbleType::None,
ChatType::Tell(_u, _) => SpeechBubbleType::Tell,
ChatType::Say(_u) => SpeechBubbleType::Say,
ChatType::Group(_u, _s) => SpeechBubbleType::Group,
ChatType::Faction(_u, _s) => SpeechBubbleType::Faction,
ChatType::Region(_u) => SpeechBubbleType::Region,
ChatType::World(_u) => SpeechBubbleType::World,
ChatType::Npc(_u, _r) => SpeechBubbleType::None,
}
}
pub fn uid(&self) -> Option<Uid> {
match &self.chat_type {
ChatType::Online => None,
ChatType::Offline => None,
ChatType::CommandInfo => None,
ChatType::CommandError => None,
ChatType::FactionMeta(_) => None,
ChatType::GroupMeta(_) => None,
ChatType::Kill => None,
ChatType::Tell(u, _t) => Some(*u),
ChatType::Say(u) => Some(*u),
ChatType::Group(u, _s) => Some(*u),
ChatType::Faction(u, _s) => Some(*u),
ChatType::Region(u) => Some(*u),
ChatType::World(u) => Some(*u),
ChatType::Npc(u, _r) => Some(*u),
}
}
}
/// Player groups are useful when forming raiding parties and coordinating
/// gameplay.
///
/// Groups are currently just an associated String (the group's name)
#[derive(Clone, Debug)]
pub struct Group(pub String);
impl Component for Group {
type Storage = IDVStorage<Self>;
}
impl From<String> for Group {
fn from(s: String) -> Self { Group(s) }
}
/// Player factions are used to coordinate pvp vs hostile factions or segment
/// chat from the world
///
/// Factions are currently just an associated String (the faction's name)
#[derive(Clone, Debug)]
pub struct Faction(pub String);
impl Component for Faction {
type Storage = IDVStorage<Self>;
}
impl From<String> for Faction {
fn from(s: String) -> Self { Faction(s) }
}
/// The contents of a speech bubble
pub enum SpeechBubbleMessage {
/// This message was said by a player and needs no translation
Plain(String),
/// This message was said by an NPC. The fields are a i18n key and a random
/// u16 index
Localized(String, u16),
}
/// List of chat types for players and NPCs. Each one has its own icon.
///
/// This is a subset of `ChatType`, and a superset of `ChatMode`
pub enum SpeechBubbleType {
// One for each chat mode
Tell,
Say,
Region,
Group,
Faction,
World,
// For NPCs
Quest, // TODO not implemented
Trade, // TODO not implemented
None, // No icon (default for npcs)
}
/// Adds a speech bubble above the character
pub struct SpeechBubble {
pub message: SpeechBubbleMessage,
pub icon: SpeechBubbleType,
pub timeout: Instant,
}
impl SpeechBubble {
/// Default duration in seconds of speech bubbles
pub const DEFAULT_DURATION: f64 = 5.0;
pub fn npc_new(i18n_key: &str, r: u16, icon: SpeechBubbleType) -> Self {
let message = SpeechBubbleMessage::Localized(i18n_key.to_string(), r);
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
timeout,
icon,
}
}
pub fn player_new(message: &str, icon: SpeechBubbleType) -> Self {
let message = SpeechBubbleMessage::Plain(message.to_string());
let timeout = Instant::now() + Duration::from_secs_f64(SpeechBubble::DEFAULT_DURATION);
Self {
message,
timeout,
icon,
}
}
pub fn message<F>(&self, i18n_variation: F) -> String
where
F: Fn(&str, u16) -> String,
{
match &self.message {
SpeechBubbleMessage::Plain(m) => m.to_string(),
SpeechBubbleMessage::Localized(k, i) => i18n_variation(&k, *i),
}
}
}

View File

@ -3,6 +3,7 @@ mod admin;
pub mod agent; pub mod agent;
mod body; mod body;
mod character_state; mod character_state;
mod chat;
mod controller; mod controller;
mod energy; mod energy;
mod inputs; mod inputs;
@ -17,13 +18,14 @@ mod visual;
// Reexports // Reexports
pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout};
pub use admin::Admin; pub use admin::{Admin, AdminList};
pub use agent::{Agent, Alignment, SpeechBubble, SPEECH_BUBBLE_DURATION}; pub use agent::{Agent, Alignment};
pub use body::{ pub use body::{
biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, golem, biped_large, bird_medium, bird_small, critter, dragon, fish_medium, fish_small, golem,
humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData, humanoid, object, quadruped_medium, quadruped_small, AllBodies, Body, BodyData,
}; };
pub use character_state::{Attacking, CharacterState, StateUpdate}; pub use character_state::{Attacking, CharacterState, StateUpdate};
pub use chat::{ChatMode, ChatMsg, ChatType, Faction, Group, SpeechBubble, SpeechBubbleType};
pub use controller::{ pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip, Climb, ControlAction, ControlEvent, Controller, ControllerInputs, Input, InventoryManip,
MountState, Mounting, MountState, Mounting,

View File

@ -164,6 +164,8 @@ pub enum ServerEvent {
ClientDisconnect(EcsEntity), ClientDisconnect(EcsEntity),
ChunkRequest(EcsEntity, Vec2<i32>), ChunkRequest(EcsEntity, Vec2<i32>),
ChatCmd(EcsEntity, String), ChatCmd(EcsEntity, String),
/// Send a chat message to the player from an npc or other player
Chat(comp::ChatMsg),
} }
pub struct EventBus<E> { pub struct EventBus<E> {

View File

@ -66,17 +66,3 @@ pub use loadout_builder::LoadoutBuilder;
/// assert_eq!("bar", scon.next_message().unwrap()); /// assert_eq!("bar", scon.next_message().unwrap());
/// ``` /// ```
pub mod net; pub mod net;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChatType {
Broadcast,
Chat,
GameUpdate,
Private,
Tell,
Say,
Group,
Faction,
Meta,
Kill,
}

View File

@ -1,6 +1,7 @@
use crate::{comp, terrain::block::Block}; use crate::{comp, terrain::block::Block};
use vek::*; use vek::*;
/// Messages sent from the client to the server
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ClientMsg { pub enum ClientMsg {
Register { Register {
@ -27,9 +28,8 @@ pub enum ClientMsg {
PlaceBlock(Vec3<i32>, Block), PlaceBlock(Vec3<i32>, Block),
Ping, Ping,
Pong, Pong,
ChatMsg { /// Send the chat message or command to be processed by the server
message: String, ChatMsg(String),
},
PlayerPhysics { PlayerPhysics {
pos: comp::Pos, pos: comp::Pos,
vel: comp::Vel, vel: comp::Vel,

View File

@ -25,7 +25,6 @@ sum_type! {
Sticky(comp::Sticky), Sticky(comp::Sticky),
Loadout(comp::Loadout), Loadout(comp::Loadout),
CharacterState(comp::CharacterState), CharacterState(comp::CharacterState),
SpeechBubble(comp::SpeechBubble),
Pos(comp::Pos), Pos(comp::Pos),
Vel(comp::Vel), Vel(comp::Vel),
Ori(comp::Ori), Ori(comp::Ori),
@ -52,7 +51,6 @@ sum_type! {
Sticky(PhantomData<comp::Sticky>), Sticky(PhantomData<comp::Sticky>),
Loadout(PhantomData<comp::Loadout>), Loadout(PhantomData<comp::Loadout>),
CharacterState(PhantomData<comp::CharacterState>), CharacterState(PhantomData<comp::CharacterState>),
SpeechBubble(PhantomData<comp::SpeechBubble>),
Pos(PhantomData<comp::Pos>), Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>), Vel(PhantomData<comp::Vel>),
Ori(PhantomData<comp::Ori>), Ori(PhantomData<comp::Ori>),
@ -79,7 +77,6 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Loadout(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Loadout(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::SpeechBubble(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Pos(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Ori(comp) => sync::handle_insert(comp, entity, world),
@ -104,7 +101,6 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Loadout(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Loadout(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::SpeechBubble(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Pos(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Vel(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Ori(comp) => sync::handle_modify(comp, entity, world),
@ -133,9 +129,6 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPhantom::CharacterState(_) => { EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world) sync::handle_remove::<comp::CharacterState>(entity, world)
}, },
EcsCompPhantom::SpeechBubble(_) => {
sync::handle_remove::<comp::SpeechBubble>(entity, world)
},
EcsCompPhantom::Pos(_) => sync::handle_remove::<comp::Pos>(entity, world), EcsCompPhantom::Pos(_) => sync::handle_remove::<comp::Pos>(entity, world),
EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world), EcsCompPhantom::Vel(_) => sync::handle_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_remove::<comp::Ori>(entity, world), EcsCompPhantom::Ori(_) => sync::handle_remove::<comp::Ori>(entity, world),

View File

@ -2,8 +2,8 @@ use super::{ClientState, EcsCompPacket};
use crate::{ use crate::{
character::CharacterItem, character::CharacterItem,
comp, state, sync, comp, state, sync,
sync::Uid,
terrain::{Block, TerrainChunk}, terrain::{Block, TerrainChunk},
ChatType,
}; };
use authc::AuthClientError; use authc::AuthClientError;
use hashbrown::HashMap; use hashbrown::HashMap;
@ -18,18 +18,22 @@ pub struct ServerInfo {
pub auth_provider: Option<String>, pub auth_provider: Option<String>,
} }
/// Inform the client of updates to the player list.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PlayerListUpdate { pub enum PlayerListUpdate {
Init(HashMap<u64, PlayerInfo>), Init(HashMap<Uid, PlayerInfo>),
Add(u64, PlayerInfo), Add(Uid, PlayerInfo),
SelectedCharacter(u64, CharacterInfo), SelectedCharacter(Uid, CharacterInfo),
LevelChange(u64, u32), LevelChange(Uid, u32),
Remove(u64), Admin(Uid, bool),
Alias(u64, String), Remove(Uid),
Alias(Uid, String),
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlayerInfo { pub struct PlayerInfo {
pub is_admin: bool,
pub is_online: bool,
pub player_alias: String, pub player_alias: String,
pub character: Option<CharacterInfo>, pub character: Option<CharacterInfo>,
} }
@ -45,6 +49,7 @@ pub enum Notification {
WaypointSaved, WaypointSaved,
} }
/// Messages sent from the server to the client
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ServerMsg { pub enum ServerMsg {
InitialSync { InitialSync {
@ -66,16 +71,15 @@ pub enum ServerMsg {
ExitIngameCleanup, ExitIngameCleanup,
Ping, Ping,
Pong, Pong,
ChatMsg { /// A message to go into the client chat box. The client is responsible for
chat_type: ChatType, /// formatting the message and turning it into a speech bubble.
message: String, ChatMsg(comp::ChatMsg),
}, SetPlayerEntity(Uid),
SetPlayerEntity(u64),
TimeOfDay(state::TimeOfDay), TimeOfDay(state::TimeOfDay),
EntitySync(sync::EntitySyncPackage), EntitySync(sync::EntitySyncPackage),
CompSync(sync::CompSyncPackage<EcsCompPacket>), CompSync(sync::CompSyncPackage<EcsCompPacket>),
CreateEntity(sync::EntityPackage<EcsCompPacket>), CreateEntity(sync::EntityPackage<EcsCompPacket>),
DeleteEntity(u64), DeleteEntity(Uid),
InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent), InventoryUpdate(comp::Inventory, comp::InventoryUpdateEvent),
TerrainChunkUpdate { TerrainChunkUpdate {
key: Vec2<i32>, key: Vec2<i32>,
@ -112,46 +116,6 @@ impl From<AuthClientError> for RegisterError {
fn from(err: AuthClientError) -> Self { Self::AuthError(err.to_string()) } fn from(err: AuthClientError) -> Self { Self::AuthError(err.to_string()) }
} }
impl ServerMsg { impl From<comp::ChatMsg> for ServerMsg {
pub fn chat(message: String) -> ServerMsg { fn from(v: comp::ChatMsg) -> Self { ServerMsg::ChatMsg(v) }
ServerMsg::ChatMsg {
chat_type: ChatType::Chat,
message,
}
}
pub fn tell(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Tell,
message,
}
}
pub fn game(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::GameUpdate,
message,
}
}
pub fn broadcast(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Broadcast,
message,
}
}
pub fn private(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Private,
message,
}
}
pub fn kill(message: String) -> ServerMsg {
ServerMsg::ChatMsg {
chat_type: ChatType::Kill,
message,
}
}
} }

View File

@ -122,7 +122,6 @@ impl State {
ecs.register::<comp::Sticky>(); ecs.register::<comp::Sticky>();
ecs.register::<comp::Gravity>(); ecs.register::<comp::Gravity>();
ecs.register::<comp::CharacterState>(); ecs.register::<comp::CharacterState>();
ecs.register::<comp::SpeechBubble>();
// Register components send from clients -> server // Register components send from clients -> server
ecs.register::<comp::Controller>(); ecs.register::<comp::Controller>();
@ -155,6 +154,9 @@ impl State {
ecs.register::<comp::Projectile>(); ecs.register::<comp::Projectile>();
ecs.register::<comp::Attacking>(); ecs.register::<comp::Attacking>();
ecs.register::<comp::ItemDrop>(); ecs.register::<comp::ItemDrop>();
ecs.register::<comp::ChatMode>();
ecs.register::<comp::Group>();
ecs.register::<comp::Faction>();
// Register synced resources used by the ECS. // Register synced resources used by the ECS.
ecs.insert(TimeOfDay(0.0)); ecs.insert(TimeOfDay(0.0));

View File

@ -3,12 +3,13 @@ use crate::{
self, self,
agent::Activity, agent::Activity,
item::{tool::ToolKind, ItemKind}, item::{tool::ToolKind, ItemKind},
Agent, Alignment, CharacterState, ControlAction, Controller, Loadout, MountState, Ori, Pos, Agent, Alignment, CharacterState, ChatMsg, ControlAction, Controller, Loadout, MountState,
Scale, SpeechBubble, Stats, Ori, Pos, Scale, Stats,
}, },
event::{EventBus, ServerEvent},
path::Chaser, path::Chaser,
state::{DeltaTime, Time}, state::{DeltaTime, Time},
sync::UidAllocator, sync::{Uid, UidAllocator},
terrain::TerrainGrid, terrain::TerrainGrid,
util::Dir, util::Dir,
vol::ReadVol, vol::ReadVol,
@ -16,7 +17,7 @@ use crate::{
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use specs::{ use specs::{
saveload::{Marker, MarkerAllocator}, saveload::{Marker, MarkerAllocator},
Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage, Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage,
}; };
use vek::*; use vek::*;
@ -28,6 +29,7 @@ impl<'a> System<'a> for Sys {
Read<'a, UidAllocator>, Read<'a, UidAllocator>,
Read<'a, Time>, Read<'a, Time>,
Read<'a, DeltaTime>, Read<'a, DeltaTime>,
Write<'a, EventBus<ServerEvent>>,
Entities<'a>, Entities<'a>,
ReadStorage<'a, Pos>, ReadStorage<'a, Pos>,
ReadStorage<'a, Ori>, ReadStorage<'a, Ori>,
@ -35,11 +37,11 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Stats>, ReadStorage<'a, Stats>,
ReadStorage<'a, Loadout>, ReadStorage<'a, Loadout>,
ReadStorage<'a, CharacterState>, ReadStorage<'a, CharacterState>,
ReadStorage<'a, Uid>,
ReadExpect<'a, TerrainGrid>, ReadExpect<'a, TerrainGrid>,
ReadStorage<'a, Alignment>, ReadStorage<'a, Alignment>,
WriteStorage<'a, Agent>, WriteStorage<'a, Agent>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
ReadStorage<'a, MountState>, ReadStorage<'a, MountState>,
); );
@ -50,6 +52,7 @@ impl<'a> System<'a> for Sys {
uid_allocator, uid_allocator,
time, time,
dt, dt,
event_bus,
entities, entities,
positions, positions,
orientations, orientations,
@ -57,11 +60,11 @@ impl<'a> System<'a> for Sys {
stats, stats,
loadouts, loadouts,
character_states, character_states,
uids,
terrain, terrain,
alignments, alignments,
mut agents, mut agents,
mut controllers, mut controllers,
mut speech_bubbles,
mount_states, mount_states,
): Self::SystemData, ): Self::SystemData,
) { ) {
@ -72,6 +75,7 @@ impl<'a> System<'a> for Sys {
alignment, alignment,
loadout, loadout,
character_state, character_state,
uid,
agent, agent,
controller, controller,
mount_state, mount_state,
@ -82,6 +86,7 @@ impl<'a> System<'a> for Sys {
alignments.maybe(), alignments.maybe(),
&loadouts, &loadouts,
&character_states, &character_states,
&uids,
&mut agents, &mut agents,
&mut controllers, &mut controllers,
mount_states.maybe(), mount_states.maybe(),
@ -386,10 +391,9 @@ impl<'a> System<'a> for Sys {
{ {
if stats.get(attacker).map_or(false, |a| !a.is_dead) { if stats.get(attacker).map_or(false, |a| !a.is_dead) {
if agent.can_speak { if agent.can_speak {
let message = let msg = "npc.speech.villager_under_attack".to_string();
"npc.speech.villager_under_attack".to_string(); event_bus
let bubble = SpeechBubble::npc_new(message, *time); .emit_now(ServerEvent::Chat(ChatMsg::npc(*uid, msg)));
let _ = speech_bubbles.insert(entity, bubble);
} }
agent.activity = Activity::Attack { agent.activity = Activity::Attack {

View File

@ -24,6 +24,7 @@ fn main() {
// Load settings // Load settings
let settings = ServerSettings::load(); let settings = ServerSettings::load();
let server_port = &settings.gameserver_address.port();
let metrics_port = &settings.metrics_address.port(); let metrics_port = &settings.metrics_address.port();
// Create server // Create server
@ -31,6 +32,7 @@ fn main() {
info!("Server is ready to accept connections."); info!("Server is ready to accept connections.");
info!(?metrics_port, "starting metrics at port"); info!(?metrics_port, "starting metrics at port");
info!(?server_port, "starting server at port");
loop { loop {
let events = server let events = server

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
} }
.unwrap_or(format!("{} died", &player.alias)); .unwrap_or(format!("{} died", &player.alias));
state.notify_registered_clients(ServerMsg::kill(msg)); state.notify_registered_clients(comp::ChatType::Kill.server_msg(msg));
} }
{ {
@ -471,12 +471,11 @@ pub fn handle_level_up(server: &mut Server, entity: EcsEntity, new_level: u32) {
let uids = server.state.ecs().read_storage::<Uid>(); let uids = server.state.ecs().read_storage::<Uid>();
let uid = uids let uid = uids
.get(entity) .get(entity)
.expect("Failed to fetch uid component for entity.") .expect("Failed to fetch uid component for entity.");
.0;
server server
.state .state
.notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::LevelChange( .notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::LevelChange(
uid, new_level, *uid, new_level,
))); )));
} }

View File

@ -120,7 +120,7 @@ pub fn handle_possess(server: &Server, possessor_uid: Uid, possesse_uid: Uid) {
let mut clients = ecs.write_storage::<Client>(); let mut clients = ecs.write_storage::<Client>();
if clients.get_mut(possesse).is_none() { if clients.get_mut(possesse).is_none() {
if let Some(mut client) = clients.remove(possessor) { if let Some(mut client) = clients.remove(possessor) {
client.notify(ServerMsg::SetPlayerEntity(possesse_uid.into())); client.notify(ServerMsg::SetPlayerEntity(possesse_uid));
clients clients
.insert(possesse, client) .insert(possesse, client)
.err() .err()

View File

@ -1,4 +1,4 @@
use crate::Server; use crate::{state_ext::StateExt, Server};
use common::event::{EventBus, ServerEvent}; use common::event::{EventBus, ServerEvent};
use entity_creation::{ use entity_creation::{
handle_create_npc, handle_create_waypoint, handle_initialize_character, handle_create_npc, handle_create_waypoint, handle_initialize_character,
@ -38,6 +38,7 @@ impl Server {
let mut requested_chunks = Vec::new(); let mut requested_chunks = Vec::new();
let mut chat_commands = Vec::new(); let mut chat_commands = Vec::new();
let mut chat_messages = Vec::new();
let events = self let events = self
.state .state
@ -103,6 +104,9 @@ impl Server {
ServerEvent::ChatCmd(entity, cmd) => { ServerEvent::ChatCmd(entity, cmd) => {
chat_commands.push((entity, cmd)); chat_commands.push((entity, cmd));
}, },
ServerEvent::Chat(msg) => {
chat_messages.push(msg);
},
} }
} }
@ -115,6 +119,10 @@ impl Server {
self.process_chat_cmd(entity, cmd); self.process_chat_cmd(entity, cmd);
} }
for msg in chat_messages {
self.state.send_chat(msg);
}
frontend_events frontend_events
} }
} }

View File

@ -8,7 +8,7 @@ use common::{
msg::{ClientState, PlayerListUpdate, ServerMsg}, msg::{ClientState, PlayerListUpdate, ServerMsg},
sync::{Uid, UidAllocator}, sync::{Uid, UidAllocator},
}; };
use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, Join, WorldExt}; use specs::{saveload::MarkerAllocator, Builder, Entity as EcsEntity, WorldExt};
use tracing::error; use tracing::error;
pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) { pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity) {
@ -53,26 +53,17 @@ pub fn handle_client_disconnect(server: &mut Server, entity: EcsEntity) -> Event
state.read_storage::<Uid>().get(entity), state.read_storage::<Uid>().get(entity),
state.read_storage::<comp::Player>().get(entity), state.read_storage::<comp::Player>().get(entity),
) { ) {
state.notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove( state.notify_registered_clients(ServerMsg::PlayerListUpdate(PlayerListUpdate::Remove(*uid)))
(*uid).into(),
)))
} }
// Make sure to remove the player from the logged in list. (See AuthProvider) // Make sure to remove the player from the logged in list. (See AuthProvider)
// And send a disconnected message // And send a disconnected message
{ if let Some(player) = state.ecs().read_storage::<Player>().get(entity) {
let players = state.ecs().read_storage::<Player>();
let mut accounts = state.ecs().write_resource::<AuthProvider>(); let mut accounts = state.ecs().write_resource::<AuthProvider>();
let mut clients = state.ecs().write_storage::<Client>(); accounts.logout(player.uuid());
if let Some(player) = players.get(entity) { let msg = comp::ChatType::Offline.server_msg(format!("[{}] went offline.", &player.alias));
accounts.logout(player.uuid()); state.notify_registered_clients(msg);
let msg = ServerMsg::broadcast(format!("{} went offline.", &player.alias));
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
}
} }
// Sync the player's character data to the database // Sync the player's character data to the database

View File

@ -29,7 +29,7 @@ use crate::{
}; };
use common::{ use common::{
cmd::ChatCommand, cmd::ChatCommand,
comp, comp::{self, ChatType},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
msg::{ClientMsg, ClientState, ServerInfo, ServerMsg}, msg::{ClientMsg, ClientState, ServerInfo, ServerMsg},
net::PostOffice, net::PostOffice,
@ -105,6 +105,14 @@ impl Server {
state state
.ecs_mut() .ecs_mut()
.insert(CharacterLoader::new(settings.persistence_db_dir.clone())); .insert(CharacterLoader::new(settings.persistence_db_dir.clone()));
state
.ecs_mut()
.insert(persistence::character::CharacterUpdater::new(
settings.persistence_db_dir.clone(),
));
state
.ecs_mut()
.insert(comp::AdminList(settings.admins.clone()));
// System timers for performance monitoring // System timers for performance monitoring
state.ecs_mut().insert(sys::EntitySyncTimer::default()); state.ecs_mut().insert(sys::EntitySyncTimer::default());
@ -114,7 +122,6 @@ impl Server {
state.ecs_mut().insert(sys::TerrainSyncTimer::default()); state.ecs_mut().insert(sys::TerrainSyncTimer::default());
state.ecs_mut().insert(sys::TerrainTimer::default()); state.ecs_mut().insert(sys::TerrainTimer::default());
state.ecs_mut().insert(sys::WaypointTimer::default()); state.ecs_mut().insert(sys::WaypointTimer::default());
state.ecs_mut().insert(sys::SpeechBubbleTimer::default());
state.ecs_mut().insert(sys::PersistenceTimer::default()); state.ecs_mut().insert(sys::PersistenceTimer::default());
// System schedulers to control execution of systems // System schedulers to control execution of systems
@ -623,9 +630,12 @@ impl Server {
Ok(frontend_events) Ok(frontend_events)
} }
pub fn notify_client(&self, entity: EcsEntity, msg: ServerMsg) { pub fn notify_client<S>(&self, entity: EcsEntity, msg: S)
where
S: Into<ServerMsg>,
{
if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) { if let Some(client) = self.state.ecs().write_storage::<Client>().get_mut(entity) {
client.notify(msg) client.notify(msg.into())
} }
} }
@ -650,7 +660,7 @@ impl Server {
} else { } else {
self.notify_client( self.notify_client(
entity, entity,
ServerMsg::private(format!( ChatType::CommandError.server_msg(format!(
"Unknown command '/{}'.\nType '/help' for available commands", "Unknown command '/{}'.\nType '/help' for available commands",
kwd kwd
)), )),

View File

@ -7,10 +7,13 @@ use common::{
effect::Effect, effect::Effect,
msg::{CharacterInfo, ClientState, PlayerListUpdate, ServerMsg}, msg::{CharacterInfo, ClientState, PlayerListUpdate, ServerMsg},
state::State, state::State,
sync::{Uid, WorldSyncExt}, sync::{Uid, UidAllocator, WorldSyncExt},
util::Dir, util::Dir,
}; };
use specs::{Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, Join, WorldExt}; use specs::{
saveload::MarkerAllocator, Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder,
Join, WorldExt,
};
use tracing::warn; use tracing::warn;
use vek::*; use vek::*;
@ -43,6 +46,7 @@ pub trait StateExt {
/// Performed after loading component data from the database /// Performed after loading component data from the database
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents); fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents);
/// Iterates over registered clients and send each `ServerMsg` /// Iterates over registered clients and send each `ServerMsg`
fn send_chat(&self, msg: comp::ChatMsg);
fn notify_registered_clients(&self, msg: ServerMsg); fn notify_registered_clients(&self, msg: ServerMsg);
/// Delete an entity, recording the deletion in [`DeletedEntities`] /// Delete an entity, recording the deletion in [`DeletedEntities`]
fn delete_entity_recorded( fn delete_entity_recorded(
@ -201,11 +205,13 @@ impl StateExt for State {
fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) { fn update_character_data(&mut self, entity: EcsEntity, components: PersistedComponents) {
let (body, stats, inventory, loadout) = components; let (body, stats, inventory, loadout) = components;
// Make sure physics are accepted.
self.write_component(entity, comp::ForceUpdate);
// Notify clients of a player list update // Notify clients of a player list update
let client_uid = self let client_uid = self
.read_component_cloned::<Uid>(entity) .read_component_cloned::<Uid>(entity)
.map(|u| u.into()) .map(|u| u)
.expect("Client doesn't have a Uid!!!"); .expect("Client doesn't have a Uid!!!");
self.notify_registered_clients(ServerMsg::PlayerListUpdate( self.notify_registered_clients(ServerMsg::PlayerListUpdate(
@ -229,6 +235,99 @@ impl StateExt for State {
self.write_component(entity, comp::ForceUpdate); self.write_component(entity, comp::ForceUpdate);
} }
/// Send the chat message to the proper players. Say and region are limited
/// by location. Faction and group are limited by component.
fn send_chat(&self, msg: comp::ChatMsg) {
let ecs = self.ecs();
let is_within =
|target, a: &comp::Pos, b: &comp::Pos| a.0.distance_squared(b.0) < target * target;
match &msg.chat_type {
comp::ChatType::Online
| comp::ChatType::Offline
| comp::ChatType::CommandInfo
| comp::ChatType::CommandError
| comp::ChatType::Kill
| comp::ChatType::World(_) => {
self.notify_registered_clients(ServerMsg::ChatMsg(msg.clone()))
},
comp::ChatType::Tell(u, t) => {
for (client, uid) in (
&mut ecs.write_storage::<Client>(),
&ecs.read_storage::<Uid>(),
)
.join()
{
if uid == u || uid == t {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}
}
},
comp::ChatType::Say(uid) => {
let entity_opt =
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
let positions = ecs.read_storage::<comp::Pos>();
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::SAY_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}
}
}
},
comp::ChatType::Region(uid) => {
let entity_opt =
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
let positions = ecs.read_storage::<comp::Pos>();
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::REGION_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}
}
}
},
comp::ChatType::Npc(uid, _r) => {
let entity_opt =
(*ecs.read_resource::<UidAllocator>()).retrieve_entity_internal(uid.0);
let positions = ecs.read_storage::<comp::Pos>();
if let Some(speaker_pos) = entity_opt.and_then(|e| positions.get(e)) {
for (client, pos) in (&mut ecs.write_storage::<Client>(), &positions).join() {
if is_within(comp::ChatMsg::NPC_DISTANCE, pos, speaker_pos) {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}
}
}
},
comp::ChatType::FactionMeta(s) | comp::ChatType::Faction(_, s) => {
for (client, faction) in (
&mut ecs.write_storage::<Client>(),
&ecs.read_storage::<comp::Faction>(),
)
.join()
{
if s == &faction.0 {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}
}
},
comp::ChatType::GroupMeta(s) | comp::ChatType::Group(_, s) => {
for (client, group) in (
&mut ecs.write_storage::<Client>(),
&ecs.read_storage::<comp::Group>(),
)
.join()
{
if s == &group.0 {
client.notify(ServerMsg::ChatMsg(msg.clone()));
}
}
},
}
}
/// Sends the message to all connected clients
fn notify_registered_clients(&self, msg: ServerMsg) { fn notify_registered_clients(&self, msg: ServerMsg) {
for client in (&mut self.ecs().write_storage::<Client>()) for client in (&mut self.ecs().write_storage::<Client>())
.join() .join()

View File

@ -147,7 +147,7 @@ impl<'a> System<'a> for Sys {
.map(|key| !regions.contains(key)) .map(|key| !regions.contains(key))
.unwrap_or(true) .unwrap_or(true)
{ {
client.notify(ServerMsg::DeleteEntity(uid.into())); client.notify(ServerMsg::DeleteEntity(uid));
} }
} }
} }
@ -301,7 +301,7 @@ impl<'a> System<'a> for Sys {
}) })
{ {
for uid in &deleted { for uid in &deleted {
client.notify(ServerMsg::DeleteEntity(*uid)); client.notify(ServerMsg::DeleteEntity(Uid(*uid)));
} }
} }
} }

View File

@ -5,8 +5,8 @@ use crate::{
}; };
use common::{ use common::{
comp::{ comp::{
Admin, CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Player, Pos, SpeechBubble, Admin, AdminList, CanBuild, ChatMode, ChatMsg, ChatType, ControlEvent, Controller,
Stats, Vel, ForceUpdate, Ori, Player, Pos, Stats, Vel,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
msg::{ msg::{
@ -22,7 +22,6 @@ use hashbrown::HashMap;
use specs::{ use specs::{
Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage, Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage,
}; };
use tracing::warn;
/// This system will handle new messages from clients /// This system will handle new messages from clients
pub struct Sys; pub struct Sys;
@ -37,18 +36,19 @@ impl<'a> System<'a> for Sys {
Write<'a, SysTimer<Self>>, Write<'a, SysTimer<Self>>,
ReadStorage<'a, Uid>, ReadStorage<'a, Uid>,
ReadStorage<'a, CanBuild>, ReadStorage<'a, CanBuild>,
ReadStorage<'a, Admin>,
ReadStorage<'a, ForceUpdate>, ReadStorage<'a, ForceUpdate>,
ReadStorage<'a, Stats>, ReadStorage<'a, Stats>,
ReadStorage<'a, ChatMode>,
WriteExpect<'a, AuthProvider>, WriteExpect<'a, AuthProvider>,
Write<'a, BlockChange>, Write<'a, BlockChange>,
ReadExpect<'a, AdminList>,
WriteStorage<'a, Admin>,
WriteStorage<'a, Pos>, WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>, WriteStorage<'a, Vel>,
WriteStorage<'a, Ori>, WriteStorage<'a, Ori>,
WriteStorage<'a, Player>, WriteStorage<'a, Player>,
WriteStorage<'a, Client>, WriteStorage<'a, Client>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
WriteStorage<'a, SpeechBubble>,
Read<'a, ServerSettings>, Read<'a, ServerSettings>,
); );
@ -66,18 +66,19 @@ impl<'a> System<'a> for Sys {
mut timer, mut timer,
uids, uids,
can_build, can_build,
admins,
force_updates, force_updates,
stats, stats,
chat_modes,
mut accounts, mut accounts,
mut block_changes, mut block_changes,
admin_list,
mut admins,
mut positions, mut positions,
mut velocities, mut velocities,
mut orientations, mut orientations,
mut players, mut players,
mut clients, mut clients,
mut controllers, mut controllers,
mut speech_bubbles,
settings, settings,
): Self::SystemData, ): Self::SystemData,
) { ) {
@ -85,16 +86,17 @@ impl<'a> System<'a> for Sys {
let mut server_emitter = server_event_bus.emitter(); let mut server_emitter = server_event_bus.emitter();
let mut new_chat_msgs = Vec::new(); let mut new_chat_msgs: Vec<(Option<specs::Entity>, ChatMsg)> = Vec::new();
// Player list to send new players. // Player list to send new players.
let player_list = (&uids, &players, &stats) let player_list = (&uids, &players, stats.maybe(), admins.maybe())
.join() .join()
.map(|(uid, player, stats)| { .map(|(uid, player, stats, admin)| {
((*uid).into(), PlayerInfo { (*uid, PlayerInfo {
is_online: true,
is_admin: admin.is_some(),
player_alias: player.alias.clone(), player_alias: player.alias.clone(),
// TODO: player might not have a character selected character: stats.map(|stats| CharacterInfo {
character: Some(CharacterInfo {
name: stats.name.clone(), name: stats.name.clone(),
level: stats.level.level(), level: stats.level.level(),
}), }),
@ -160,7 +162,8 @@ impl<'a> System<'a> for Sys {
let vd = view_distance let vd = view_distance
.map(|vd| vd.min(settings.max_view_distance.unwrap_or(vd))); .map(|vd| vd.min(settings.max_view_distance.unwrap_or(vd)));
let player = Player::new(username, None, vd, uuid); let player = Player::new(username.clone(), None, vd, uuid);
let is_admin = admin_list.contains(&username);
if !player.is_valid() { if !player.is_valid() {
// Invalid player // Invalid player
@ -173,6 +176,12 @@ impl<'a> System<'a> for Sys {
// Add Player component to this client // Add Player component to this client
let _ = players.insert(entity, player); let _ = players.insert(entity, player);
// Give the Admin component to the player if their name exists in
// admin list
if is_admin {
let _ = admins.insert(entity, Admin);
}
// Tell the client its request was successful. // Tell the client its request was successful.
client.allow_state(ClientState::Registered); client.allow_state(ClientState::Registered);
@ -229,7 +238,11 @@ impl<'a> System<'a> for Sys {
// Become Registered first. // Become Registered first.
ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered | ClientState::Spectator => { ClientState::Registered | ClientState::Spectator => {
if let Some(player) = players.get(entity) { // Only send login message if it wasn't already
// sent previously
if let (Some(player), false) =
(players.get(entity), client.login_msg_sent)
{
// Send a request to load the character's component data from the // Send a request to load the character's component data from the
// DB. Once loaded, persisted components such as stats and inventory // DB. Once loaded, persisted components such as stats and inventory
// will be inserted for the entity // will be inserted for the entity
@ -248,21 +261,19 @@ impl<'a> System<'a> for Sys {
// Give the player a welcome message // Give the player a welcome message
if settings.server_description.len() > 0 { if settings.server_description.len() > 0 {
client.notify(ServerMsg::broadcast( client.notify(
settings.server_description.clone(), ChatType::CommandInfo
)); .server_msg(settings.server_description.clone()),
);
} }
// Only send login message if it wasn't already // Only send login message if it wasn't already
// sent previously // sent previously
if !client.login_msg_sent { if !client.login_msg_sent {
new_chat_msgs.push(( new_chat_msgs.push((None, ChatMsg {
None, chat_type: ChatType::Online,
ServerMsg::broadcast(format!( message: format!("[{}] is now online.", &player.alias),
"[{}] is now online.", }));
&player.alias
)),
));
client.login_msg_sent = true; client.login_msg_sent = true;
} }
@ -320,16 +331,28 @@ impl<'a> System<'a> for Sys {
}, },
ClientState::Pending => {}, ClientState::Pending => {},
}, },
ClientMsg::ChatMsg { message } => match client.client_state { ClientMsg::ChatMsg(message) => match client.client_state {
ClientState::Connected => client.error_state(RequestStateError::Impossible), ClientState::Connected => client.error_state(RequestStateError::Impossible),
ClientState::Registered ClientState::Registered
| ClientState::Spectator | ClientState::Spectator
| ClientState::Character => match validate_chat_msg(&message) { | ClientState::Character => match validate_chat_msg(&message) {
Ok(()) => new_chat_msgs.push((Some(entity), ServerMsg::chat(message))), Ok(()) => {
if let Some(from) = uids.get(entity) {
let mode = chat_modes.get(entity).cloned().unwrap_or_default();
let msg = mode.new_message(*from, message);
new_chat_msgs.push((Some(entity), msg));
} else {
tracing::error!("Could not send message. Missing player uid");
}
},
Err(ChatMsgValidationError::TooLong) => { Err(ChatMsgValidationError::TooLong) => {
let max = MAX_BYTES_CHAT_MSG; let max = MAX_BYTES_CHAT_MSG;
let len = message.len(); let len = message.len();
warn!(?len, ?max, "Recieved a chat message that's too long") tracing::warn!(
?len,
?max,
"Recieved a chat message that's too long"
)
}, },
}, },
ClientState::Pending => {}, ClientState::Pending => {},
@ -432,11 +455,12 @@ impl<'a> System<'a> for Sys {
// Tell all clients to add them to the player list. // Tell all clients to add them to the player list.
for entity in new_players { for entity in new_players {
if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) { if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) {
let msg = let msg = ServerMsg::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo {
ServerMsg::PlayerListUpdate(PlayerListUpdate::Add((*uid).into(), PlayerInfo { player_alias: player.alias.clone(),
player_alias: player.alias.clone(), is_online: true,
character: None, // new players will be on character select. is_admin: admins.get(entity).is_some(),
})); character: None, // new players will be on character select.
}));
for client in (&mut clients).join().filter(|c| c.is_registered()) { for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone()) client.notify(msg.clone())
} }
@ -445,45 +469,15 @@ impl<'a> System<'a> for Sys {
// Handle new chat messages. // Handle new chat messages.
for (entity, msg) in new_chat_msgs { for (entity, msg) in new_chat_msgs {
match msg { // Handle chat commands.
ServerMsg::ChatMsg { chat_type, message } => { if msg.message.starts_with("/") {
let message = if let Some(entity) = entity { if let (Some(entity), true) = (entity, msg.message.len() > 1) {
// Handle chat commands. let argv = String::from(&msg.message[1..]);
if message.starts_with("/") && message.len() > 1 { server_emitter.emit(ServerEvent::ChatCmd(entity, argv));
let argv = String::from(&message[1..]); }
server_emitter.emit(ServerEvent::ChatCmd(entity, argv)); } else {
continue; // Send chat message
} else { server_emitter.emit(ServerEvent::Chat(msg));
let bubble = SpeechBubble::player_new(message.clone(), *time);
let _ = speech_bubbles.insert(entity, bubble);
format!(
"{}[{}] {}: {}",
match admins.get(entity) {
Some(_) => "[ADMIN]",
None => "",
},
match players.get(entity) {
Some(player) => &player.alias,
None => "<Unknown>",
},
match stats.get(entity) {
Some(stat) => &stat.name,
None => "<Unknown>",
},
message
)
}
} else {
message
};
let msg = ServerMsg::ChatMsg { chat_type, message };
for client in (&mut clients).join().filter(|c| c.is_registered()) {
client.notify(msg.clone());
}
},
_ => {
panic!("Invalid message type.");
},
} }
} }

View File

@ -2,7 +2,6 @@ pub mod entity_sync;
pub mod message; pub mod message;
pub mod persistence; pub mod persistence;
pub mod sentinel; pub mod sentinel;
pub mod speech_bubble;
pub mod subscription; pub mod subscription;
pub mod terrain; pub mod terrain;
pub mod terrain_sync; pub mod terrain_sync;
@ -21,7 +20,6 @@ pub type SubscriptionTimer = SysTimer<subscription::Sys>;
pub type TerrainTimer = SysTimer<terrain::Sys>; pub type TerrainTimer = SysTimer<terrain::Sys>;
pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>; pub type TerrainSyncTimer = SysTimer<terrain_sync::Sys>;
pub type WaypointTimer = SysTimer<waypoint::Sys>; pub type WaypointTimer = SysTimer<waypoint::Sys>;
pub type SpeechBubbleTimer = SysTimer<speech_bubble::Sys>;
pub type PersistenceTimer = SysTimer<persistence::Sys>; pub type PersistenceTimer = SysTimer<persistence::Sys>;
pub type PersistenceScheduler = SysScheduler<persistence::Sys>; pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
@ -33,13 +31,11 @@ pub type PersistenceScheduler = SysScheduler<persistence::Sys>;
//const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys"; //const TERRAIN_SYNC_SYS: &str = "server_terrain_sync_sys";
const TERRAIN_SYS: &str = "server_terrain_sys"; const TERRAIN_SYS: &str = "server_terrain_sys";
const WAYPOINT_SYS: &str = "waypoint_sys"; const WAYPOINT_SYS: &str = "waypoint_sys";
const SPEECH_BUBBLE_SYS: &str = "speech_bubble_sys";
const PERSISTENCE_SYS: &str = "persistence_sys"; const PERSISTENCE_SYS: &str = "persistence_sys";
pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) {
dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]); dispatch_builder.add(terrain::Sys, TERRAIN_SYS, &[]);
dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]); dispatch_builder.add(waypoint::Sys, WAYPOINT_SYS, &[]);
dispatch_builder.add(speech_bubble::Sys, SPEECH_BUBBLE_SYS, &[]);
dispatch_builder.add(persistence::Sys, PERSISTENCE_SYS, &[]); dispatch_builder.add(persistence::Sys, PERSISTENCE_SYS, &[]);
} }

View File

@ -2,7 +2,7 @@ use super::SysTimer;
use common::{ use common::{
comp::{ comp::{
Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, Loadout, Body, CanBuild, CharacterState, Collider, Energy, Gravity, Item, LightEmitter, Loadout,
Mass, MountState, Mounting, Ori, Player, Pos, Scale, SpeechBubble, Stats, Sticky, Vel, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Stats, Sticky, Vel,
}, },
msg::EcsCompPacket, msg::EcsCompPacket,
sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt}, sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, Uid, UpdateTracker, WorldSyncExt},
@ -54,7 +54,6 @@ pub struct TrackedComps<'a> {
pub gravity: ReadStorage<'a, Gravity>, pub gravity: ReadStorage<'a, Gravity>,
pub loadout: ReadStorage<'a, Loadout>, pub loadout: ReadStorage<'a, Loadout>,
pub character_state: ReadStorage<'a, CharacterState>, pub character_state: ReadStorage<'a, CharacterState>,
pub speech_bubble: ReadStorage<'a, SpeechBubble>,
} }
impl<'a> TrackedComps<'a> { impl<'a> TrackedComps<'a> {
pub fn create_entity_package( pub fn create_entity_package(
@ -126,10 +125,6 @@ impl<'a> TrackedComps<'a> {
.get(entity) .get(entity)
.cloned() .cloned()
.map(|c| comps.push(c.into())); .map(|c| comps.push(c.into()));
self.speech_bubble
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
// Add untracked comps // Add untracked comps
pos.map(|c| comps.push(c.into())); pos.map(|c| comps.push(c.into()));
vel.map(|c| comps.push(c.into())); vel.map(|c| comps.push(c.into()));
@ -157,7 +152,6 @@ pub struct ReadTrackers<'a> {
pub gravity: ReadExpect<'a, UpdateTracker<Gravity>>, pub gravity: ReadExpect<'a, UpdateTracker<Gravity>>,
pub loadout: ReadExpect<'a, UpdateTracker<Loadout>>, pub loadout: ReadExpect<'a, UpdateTracker<Loadout>>,
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>, pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub speech_bubble: ReadExpect<'a, UpdateTracker<SpeechBubble>>,
} }
impl<'a> ReadTrackers<'a> { impl<'a> ReadTrackers<'a> {
pub fn create_sync_packages( pub fn create_sync_packages(
@ -194,12 +188,6 @@ impl<'a> ReadTrackers<'a> {
&*self.character_state, &*self.character_state,
&comps.character_state, &comps.character_state,
filter, filter,
)
.with_component(
&comps.uid,
&*self.speech_bubble,
&comps.speech_bubble,
filter,
); );
(entity_sync_package, comp_sync_package) (entity_sync_package, comp_sync_package)
@ -225,7 +213,6 @@ pub struct WriteTrackers<'a> {
gravity: WriteExpect<'a, UpdateTracker<Gravity>>, gravity: WriteExpect<'a, UpdateTracker<Gravity>>,
loadout: WriteExpect<'a, UpdateTracker<Loadout>>, loadout: WriteExpect<'a, UpdateTracker<Loadout>>,
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>, character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
speech_bubble: WriteExpect<'a, UpdateTracker<SpeechBubble>>,
} }
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
@ -249,7 +236,6 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers trackers
.character_state .character_state
.record_changes(&comps.character_state); .record_changes(&comps.character_state);
trackers.speech_bubble.record_changes(&comps.speech_bubble);
} }
pub fn register_trackers(world: &mut World) { pub fn register_trackers(world: &mut World) {
@ -270,7 +256,6 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Gravity>(); world.register_tracker::<Gravity>();
world.register_tracker::<Loadout>(); world.register_tracker::<Loadout>();
world.register_tracker::<CharacterState>(); world.register_tracker::<CharacterState>();
world.register_tracker::<SpeechBubble>();
} }
/// Deleted entities grouped by region /// Deleted entities grouped by region

View File

@ -1,29 +0,0 @@
use super::SysTimer;
use common::{comp::SpeechBubble, state::Time};
use specs::{Entities, Join, Read, System, Write, WriteStorage};
/// This system removes timed-out speech bubbles
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, Time>,
WriteStorage<'a, SpeechBubble>,
Write<'a, SysTimer<Self>>,
);
fn run(&mut self, (entities, time, mut speech_bubbles, mut timer): Self::SystemData) {
timer.start();
let expired_ents: Vec<_> = (&entities, &mut speech_bubbles)
.join()
.filter(|(_, speech_bubble)| speech_bubble.timeout.map_or(true, |t| t.0 < time.0))
.map(|(ent, _)| ent)
.collect();
for ent in expired_ents {
speech_bubbles.remove(ent);
}
timer.end();
}
}

View File

@ -151,7 +151,7 @@ impl<'a> System<'a> for Sys {
.map(|key| subscription.regions.contains(key)) .map(|key| subscription.regions.contains(key))
.unwrap_or(false) .unwrap_or(false)
{ {
client.notify(ServerMsg::DeleteEntity(uid.into())); client.notify(ServerMsg::DeleteEntity(uid));
} }
} }
}, },
@ -159,7 +159,7 @@ impl<'a> System<'a> for Sys {
} }
// Tell client to delete entities in the region // Tell client to delete entities in the region
for (&uid, _) in (&uids, region.entities()).join() { for (&uid, _) in (&uids, region.entities()).join() {
client.notify(ServerMsg::DeleteEntity(uid.into())); client.notify(ServerMsg::DeleteEntity(uid));
} }
} }
// Send deleted entities since they won't be processed for this client in entity // Send deleted entities since they won't be processed for this client in entity
@ -169,7 +169,7 @@ impl<'a> System<'a> for Sys {
.iter() .iter()
.flat_map(|v| v.iter()) .flat_map(|v| v.iter())
{ {
client.notify(ServerMsg::DeleteEntity(*uid)); client.notify(ServerMsg::DeleteEntity(Uid(*uid)));
} }
} }

View File

@ -1,10 +1,13 @@
use super::{ use super::{
img_ids::Imgs, BROADCAST_COLOR, FACTION_COLOR, GAME_UPDATE_COLOR, GROUP_COLOR, KILL_COLOR, img_ids::Imgs, ERROR_COLOR, FACTION_COLOR, GROUP_COLOR, INFO_COLOR, KILL_COLOR, OFFLINE_COLOR,
META_COLOR, PRIVATE_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, WORLD_COLOR,
}; };
use crate::{ui::fonts::ConrodVoxygenFonts, GlobalState}; use crate::{ui::fonts::ConrodVoxygenFonts, GlobalState};
use client::{cmd, Client, Event as ClientEvent}; use client::{cmd, Client};
use common::{msg::validate_chat_msg, ChatType}; use common::{
comp::{ChatMsg, ChatType},
msg::validate_chat_msg,
};
use conrod_core::{ use conrod_core::{
input::Key, input::Key,
position::Dimension, position::Dimension,
@ -12,8 +15,8 @@ use conrod_core::{
self, self,
cursor::{self, Index}, cursor::{self, Index},
}, },
widget::{self, Button, Id, List, Rectangle, Text, TextEdit}, widget::{self, Button, Id, Image, List, Rectangle, Text, TextEdit},
widget_ids, Colorable, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon, widget_ids, Color, Colorable, Positionable, Sizeable, Ui, UiCell, Widget, WidgetCommon,
}; };
use std::collections::VecDeque; use std::collections::VecDeque;
@ -24,15 +27,20 @@ widget_ids! {
chat_input, chat_input,
chat_input_bg, chat_input_bg,
chat_arrow, chat_arrow,
completion_box, chat_icons[],
} }
} }
const MAX_MESSAGES: usize = 100; const MAX_MESSAGES: usize = 100;
const CHAT_BOX_WIDTH: f64 = 470.0;
const CHAT_BOX_INPUT_WIDTH: f64 = 460.0;
const CHAT_BOX_HEIGHT: f64 = 174.0;
#[derive(WidgetCommon)] #[derive(WidgetCommon)]
pub struct Chat<'a> { pub struct Chat<'a> {
new_messages: &'a mut VecDeque<ClientEvent>, new_messages: &'a mut VecDeque<ChatMsg>,
client: &'a Client,
force_input: Option<String>, force_input: Option<String>,
force_cursor: Option<Index>, force_cursor: Option<Index>,
force_completions: Option<Vec<String>>, force_completions: Option<Vec<String>>,
@ -50,13 +58,15 @@ pub struct Chat<'a> {
impl<'a> Chat<'a> { impl<'a> Chat<'a> {
pub fn new( pub fn new(
new_messages: &'a mut VecDeque<ClientEvent>, new_messages: &'a mut VecDeque<ChatMsg>,
client: &'a Client,
global_state: &'a GlobalState, global_state: &'a GlobalState,
imgs: &'a Imgs, imgs: &'a Imgs,
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
) -> Self { ) -> Self {
Self { Self {
new_messages, new_messages,
client,
force_input: None, force_input: None,
force_cursor: None, force_cursor: None,
force_completions: None, force_completions: None,
@ -68,9 +78,9 @@ impl<'a> Chat<'a> {
} }
} }
pub fn prepare_tab_completion(mut self, input: String, client: &Client) -> Self { pub fn prepare_tab_completion(mut self, input: String) -> Self {
if let Some(index) = input.find('\t') { if let Some(index) = input.find('\t') {
self.force_completions = Some(cmd::complete(&input[..index], &client)); self.force_completions = Some(cmd::complete(&input[..index], &self.client));
} else { } else {
self.force_completions = None; self.force_completions = None;
} }
@ -105,7 +115,7 @@ impl<'a> Chat<'a> {
} }
pub struct State { pub struct State {
messages: VecDeque<ClientEvent>, messages: VecDeque<ChatMsg>,
input: String, input: String,
ids: Ids, ids: Ids,
history: VecDeque<String>, history: VecDeque<String>,
@ -259,7 +269,7 @@ impl<'a> Widget for Chat<'a> {
// Any changes to this TextEdit's width and font size must be reflected in // Any changes to this TextEdit's width and font size must be reflected in
// `cursor_offset_to_index` below. // `cursor_offset_to_index` below.
let mut text_edit = TextEdit::new(&state.input) let mut text_edit = TextEdit::new(&state.input)
.w(460.0) .w(CHAT_BOX_INPUT_WIDTH)
.restrict_to_height(false) .restrict_to_height(false)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.line_spacing(2.0) .line_spacing(2.0)
@ -274,10 +284,10 @@ impl<'a> Widget for Chat<'a> {
Dimension::Absolute(y) => y + 6.0, Dimension::Absolute(y) => y + 6.0,
_ => 0.0, _ => 0.0,
}; };
Rectangle::fill([470.0, y]) Rectangle::fill([CHAT_BOX_WIDTH, y])
.rgba(0.0, 0.0, 0.0, transp + 0.1) .rgba(0.0, 0.0, 0.0, transp + 0.1)
.bottom_left_with_margins_on(ui.window, 10.0, 10.0) .bottom_left_with_margins_on(ui.window, 10.0, 10.0)
.w(470.0) .w(CHAT_BOX_WIDTH)
.set(state.ids.chat_input_bg, ui); .set(state.ids.chat_input_bg, ui);
if let Some(str) = text_edit if let Some(str) = text_edit
@ -293,7 +303,7 @@ impl<'a> Widget for Chat<'a> {
} }
// Message box // Message box
Rectangle::fill([470.0, 174.0]) Rectangle::fill([CHAT_BOX_WIDTH, CHAT_BOX_HEIGHT])
.rgba(0.0, 0.0, 0.0, transp) .rgba(0.0, 0.0, 0.0, transp)
.and(|r| { .and(|r| {
if input_focused { if input_focused {
@ -302,61 +312,57 @@ impl<'a> Widget for Chat<'a> {
r.bottom_left_with_margins_on(ui.window, 10.0, 10.0) r.bottom_left_with_margins_on(ui.window, 10.0, 10.0)
} }
}) })
.crop_kids()
.set(state.ids.message_box_bg, ui); .set(state.ids.message_box_bg, ui);
let (mut items, _) = List::flow_down(state.messages.len() + 1) let (mut items, _) = List::flow_down(state.messages.len() + 1)
.top_left_of(state.ids.message_box_bg) .top_left_with_margins_on(state.ids.message_box_bg, 0.0, 16.0)
.w_h(470.0, 174.0) .w_h(CHAT_BOX_WIDTH, CHAT_BOX_HEIGHT)
.scroll_kids_vertically() .scroll_kids_vertically()
.set(state.ids.message_box, ui); .set(state.ids.message_box, ui);
if state.ids.chat_icons.len() < state.messages.len() {
state.update(|s| {
s.ids
.chat_icons
.resize(s.messages.len(), &mut ui.widget_id_generator())
});
}
let show_char_name = self.global_state.settings.gameplay.chat_character_name;
while let Some(item) = items.next(ui) { while let Some(item) = items.next(ui) {
// This would be easier if conrod used the v-metrics from rusttype. // This would be easier if conrod used the v-metrics from rusttype.
let widget = if item.i < state.messages.len() { if item.i < state.messages.len() {
let msg = &state.messages[item.i]; let message = &state.messages[item.i];
match msg { let (color, icon) = render_chat_line(&message.chat_type, &self.imgs);
ClientEvent::Chat { chat_type, message } => { let msg = self.client.format_message(message, show_char_name);
let color = match chat_type { let text = Text::new(&msg)
ChatType::Meta => META_COLOR, .font_size(self.fonts.opensans.scale(15))
ChatType::Tell => TELL_COLOR, .font_id(self.fonts.opensans.conrod_id)
ChatType::Chat => TEXT_COLOR, .w(CHAT_BOX_WIDTH - 16.0)
ChatType::Private => PRIVATE_COLOR, .color(color)
ChatType::Broadcast => BROADCAST_COLOR, .line_spacing(2.0);
ChatType::GameUpdate => GAME_UPDATE_COLOR, // Add space between messages.
ChatType::Say => SAY_COLOR, let y = match text.get_y_dimension(ui) {
ChatType::Group => GROUP_COLOR, Dimension::Absolute(y) => y + 2.0,
ChatType::Faction => FACTION_COLOR, _ => 0.0,
ChatType::Kill => KILL_COLOR, };
}; item.set(text.h(y), ui);
let text = Text::new(&message) let icon_id = state.ids.chat_icons[item.i];
.font_size(self.fonts.opensans.scale(15)) Image::new(icon)
.font_id(self.fonts.opensans.conrod_id) .w_h(16.0, 16.0)
.w(470.0) .top_left_with_margins_on(item.widget_id, 2.0, -16.0)
.color(color) .parent(state.ids.message_box_bg)
.line_spacing(2.0); .set(icon_id, ui);
// Add space between messages.
let y = match text.get_y_dimension(ui) {
Dimension::Absolute(y) => y + 2.0,
_ => 0.0,
};
Some(text.h(y))
},
_ => None,
}
} else { } else {
// Spacer at bottom of the last message so that it is not cut off. // Spacer at bottom of the last message so that it is not cut off.
// Needs to be larger than the space above. // Needs to be larger than the space above.
Some( item.set(
Text::new("") Text::new("")
.font_size(self.fonts.opensans.scale(6)) .font_size(self.fonts.opensans.scale(6))
.font_id(self.fonts.opensans.conrod_id) .font_id(self.fonts.opensans.conrod_id)
.w(470.0), .w(CHAT_BOX_WIDTH),
) ui,
);
}; };
match widget {
Some(widget) => {
item.set(widget, ui);
},
None => {},
}
} }
// Chat Arrow // Chat Arrow
@ -367,6 +373,7 @@ impl<'a> Widget for Chat<'a> {
.hover_image(self.imgs.chat_arrow_mo) .hover_image(self.imgs.chat_arrow_mo)
.press_image(self.imgs.chat_arrow_press) .press_image(self.imgs.chat_arrow_press)
.bottom_right_with_margins_on(state.ids.message_box_bg, 0.0, -22.0) .bottom_right_with_margins_on(state.ids.message_box_bg, 0.0, -22.0)
.parent(id)
.set(state.ids.chat_arrow, ui) .set(state.ids.chat_arrow, ui)
.was_clicked() .was_clicked()
{ {
@ -417,7 +424,6 @@ fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize)
if char_i < cursor { if char_i < cursor {
pre_ws = Some(byte_i); pre_ws = Some(byte_i);
} else { } else {
assert_eq!(post_ws, None); // TODO debug
post_ws = Some(byte_i); post_ws = Some(byte_i);
break; break;
} }
@ -457,15 +463,31 @@ fn cursor_offset_to_index(
fonts: &ConrodVoxygenFonts, fonts: &ConrodVoxygenFonts,
) -> Option<Index> { ) -> Option<Index> {
// This moves the cursor to the given offset. Conrod is a pain. // This moves the cursor to the given offset. Conrod is a pain.
//let iter = cursor::xys_per_line_from_text(&text, &[], &font, font_size, //
// Justify::Left, Align::Start, 2.0, Rect{x: Range{start: 0.0, end: width}, y: // Width and font must match that of the chat TextEdit
// Range{start: 0.0, end: 12.345}});
// cursor::closest_cursor_index_and_xy([f64::MAX, f64::MAX], iter).map(|(i, _)|
// i) Width and font must match that of the chat TextEdit
let width = 460.0;
let font = ui.fonts.get(fonts.opensans.conrod_id)?; let font = ui.fonts.get(fonts.opensans.conrod_id)?;
let font_size = fonts.opensans.scale(15); let font_size = fonts.opensans.scale(15);
let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(width); let infos = text::line::infos(&text, &font, font_size).wrap_by_whitespace(CHAT_BOX_INPUT_WIDTH);
cursor::index_before_char(infos, offset) cursor::index_before_char(infos, offset)
} }
/// Get the color and icon for the current line in the chat box
fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod_core::image::Id) {
match chat_type {
ChatType::Online => (ONLINE_COLOR, imgs.chat_online_small),
ChatType::Offline => (OFFLINE_COLOR, imgs.chat_offline_small),
ChatType::CommandError => (ERROR_COLOR, imgs.chat_command_error_small),
ChatType::CommandInfo => (INFO_COLOR, imgs.chat_command_info_small),
ChatType::GroupMeta(_) => (GROUP_COLOR, imgs.chat_group_small),
ChatType::FactionMeta(_) => (FACTION_COLOR, imgs.chat_faction_small),
ChatType::Kill => (KILL_COLOR, imgs.chat_kill_small),
ChatType::Tell(_from, _to) => (TELL_COLOR, imgs.chat_tell_small),
ChatType::Say(_uid) => (SAY_COLOR, imgs.chat_say_small),
ChatType::Group(_uid, _s) => (GROUP_COLOR, imgs.chat_group_small),
ChatType::Faction(_uid, _s) => (FACTION_COLOR, imgs.chat_faction_small),
ChatType::Region(_uid) => (REGION_COLOR, imgs.chat_region_small),
ChatType::World(_uid) => (WORLD_COLOR, imgs.chat_world_small),
ChatType::Npc(_uid, _r) => panic!("NPCs can't talk"), // Should be filtered by hud/mod.rs
}
}

View File

@ -54,7 +54,6 @@ image_ids! {
chat_arrow_press: "voxygen.element.buttons.arrow_down_press", chat_arrow_press: "voxygen.element.buttons.arrow_down_press",
//////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////
<VoxelPixArtGraphic> <VoxelPixArtGraphic>
@ -286,6 +285,7 @@ image_ids! {
speech_bubble_bottom: "voxygen.element.frames.bubble.bottom", speech_bubble_bottom: "voxygen.element.frames.bubble.bottom",
speech_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right", speech_bubble_bottom_right: "voxygen.element.frames.bubble.bottom_right",
speech_bubble_tail: "voxygen.element.frames.bubble.tail", speech_bubble_tail: "voxygen.element.frames.bubble.tail",
speech_bubble_icon_frame: "voxygen.element.frames.bubble_dark.icon_frame",
dark_bubble_top_left: "voxygen.element.frames.bubble_dark.top_left", dark_bubble_top_left: "voxygen.element.frames.bubble_dark.top_left",
dark_bubble_top: "voxygen.element.frames.bubble_dark.top", dark_bubble_top: "voxygen.element.frames.bubble_dark.top",
@ -297,6 +297,28 @@ image_ids! {
dark_bubble_bottom: "voxygen.element.frames.bubble_dark.bottom", dark_bubble_bottom: "voxygen.element.frames.bubble_dark.bottom",
dark_bubble_bottom_right: "voxygen.element.frames.bubble_dark.bottom_right", dark_bubble_bottom_right: "voxygen.element.frames.bubble_dark.bottom_right",
dark_bubble_tail: "voxygen.element.frames.bubble_dark.tail", dark_bubble_tail: "voxygen.element.frames.bubble_dark.tail",
dark_bubble_icon_frame: "voxygen.element.frames.bubble_dark.icon_frame",
// Chat icons
chat_faction_small: "voxygen.element.icons.chat.faction_small",
chat_group_small: "voxygen.element.icons.chat.group_small",
chat_kill_small: "voxygen.element.icons.chat.kill_small",
chat_region_small: "voxygen.element.icons.chat.region_small",
chat_say_small: "voxygen.element.icons.chat.say_small",
chat_tell_small: "voxygen.element.icons.chat.tell_small",
chat_world_small: "voxygen.element.icons.chat.world_small",
chat_command_error_small: "voxygen.element.icons.chat.command_error_small",
chat_command_info_small: "voxygen.element.icons.chat.command_info_small",
chat_online_small: "voxygen.element.icons.chat.online_small",
chat_offline_small: "voxygen.element.icons.chat.offline_small",
chat_faction: "voxygen.element.icons.chat.faction",
chat_group: "voxygen.element.icons.chat.group",
chat_region: "voxygen.element.icons.chat.region",
chat_say: "voxygen.element.icons.chat.say",
chat_tell: "voxygen.element.icons.chat.tell",
chat_world: "voxygen.element.icons.chat.world",
<BlankGraphic> <BlankGraphic>
nothing: (), nothing: (),

View File

@ -46,15 +46,18 @@ use crate::{
window::{Event as WinEvent, GameInput}, window::{Event as WinEvent, GameInput},
GlobalState, GlobalState,
}; };
use client::{Client, Event as ClientEvent}; use client::Client;
use common::{assets::load_expect, comp, terrain::TerrainChunk, vol::RectRasterableVol}; use common::{assets::load_expect, comp, sync::Uid, terrain::TerrainChunk, vol::RectRasterableVol};
use conrod_core::{ use conrod_core::{
text::cursor::Index, text::cursor::Index,
widget::{self, Button, Image, Text}, widget::{self, Button, Image, Text},
widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget, widget_ids, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
}; };
use specs::{Join, WorldExt}; use specs::{Join, WorldExt};
use std::collections::VecDeque; use std::{
collections::{HashMap, VecDeque},
time::Instant,
};
use vek::*; use vek::*;
const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0); const XP_COLOR: Color = Color::Rgba(0.59, 0.41, 0.67, 1.0);
@ -74,15 +77,28 @@ const MANA_COLOR: Color = Color::Rgba(0.29, 0.62, 0.75, 0.9);
//const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0); //const RAGE_COLOR: Color = Color::Rgba(0.5, 0.04, 0.13, 1.0);
// Chat Colors // Chat Colors
const META_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0); /// Color for chat command errors (yellow !)
const ERROR_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0);
/// Color for chat command info (blue i)
const INFO_COLOR: Color = Color::Rgba(0.28, 0.83, 0.71, 1.0);
/// Online color
const ONLINE_COLOR: Color = Color::Rgba(0.3, 1.0, 0.3, 1.0);
/// Offline color
const OFFLINE_COLOR: Color = Color::Rgba(1.0, 0.3, 0.3, 1.0);
/// Color for a private message from another player
const TELL_COLOR: Color = Color::Rgba(0.98, 0.71, 1.0, 1.0); const TELL_COLOR: Color = Color::Rgba(0.98, 0.71, 1.0, 1.0);
const PRIVATE_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0); // Difference between private and tell? /// Color for local chat
const BROADCAST_COLOR: Color = Color::Rgba(0.28, 0.83, 0.71, 1.0); const SAY_COLOR: Color = Color::Rgba(1.0, 0.8, 0.8, 1.0);
const GAME_UPDATE_COLOR: Color = Color::Rgba(1.0, 1.0, 0.0, 1.0); /// Color for group chat
const SAY_COLOR: Color = Color::Rgba(1.0, 1.0, 1.0, 1.0);
const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0); const GROUP_COLOR: Color = Color::Rgba(0.47, 0.84, 1.0, 1.0);
/// Color for factional chat
const FACTION_COLOR: Color = Color::Rgba(0.24, 1.0, 0.48, 1.0); const FACTION_COLOR: Color = Color::Rgba(0.24, 1.0, 0.48, 1.0);
/// Color for regional chat
const REGION_COLOR: Color = Color::Rgba(0.8, 1.0, 0.8, 1.0);
/// Color for death messages
const KILL_COLOR: Color = Color::Rgba(1.0, 0.17, 0.17, 1.0); const KILL_COLOR: Color = Color::Rgba(1.0, 0.17, 0.17, 1.0);
/// Color for global messages
const WORLD_COLOR: Color = Color::Rgba(0.95, 1.0, 0.95, 1.0);
// UI Color-Theme // UI Color-Theme
const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue const UI_MAIN: Color = Color::Rgba(0.61, 0.70, 0.70, 1.0); // Greenish Blue
@ -240,6 +256,7 @@ pub enum Event {
ChangeFluidMode(FluidMode), ChangeFluidMode(FluidMode),
CrosshairTransp(f32), CrosshairTransp(f32),
ChatTransp(f32), ChatTransp(f32),
ChatCharName(bool),
CrosshairType(CrosshairType), CrosshairType(CrosshairType),
ToggleXpBar(XpBar), ToggleXpBar(XpBar),
Intro(Intro), Intro(Intro),
@ -249,6 +266,7 @@ pub enum Event {
SctPlayerBatch(bool), SctPlayerBatch(bool),
SctDamageBatch(bool), SctDamageBatch(bool),
SpeechBubbleDarkMode(bool), SpeechBubbleDarkMode(bool),
SpeechBubbleIcon(bool),
ToggleDebug(bool), ToggleDebug(bool),
UiScale(ScaleChange), UiScale(ScaleChange),
CharacterSelection, CharacterSelection,
@ -454,7 +472,9 @@ pub struct Hud {
item_imgs: ItemImgs, item_imgs: ItemImgs,
fonts: ConrodVoxygenFonts, fonts: ConrodVoxygenFonts,
rot_imgs: ImgsRot, rot_imgs: ImgsRot,
new_messages: VecDeque<ClientEvent>, new_messages: VecDeque<comp::ChatMsg>,
new_notifications: VecDeque<common::msg::Notification>,
speech_bubbles: HashMap<Uid, comp::SpeechBubble>,
show: Show, show: Show,
//never_show: bool, //never_show: bool,
//intro: bool, //intro: bool,
@ -520,6 +540,8 @@ impl Hud {
fonts, fonts,
ids, ids,
new_messages: VecDeque::new(), new_messages: VecDeque::new(),
new_notifications: VecDeque::new(),
speech_bubbles: HashMap::new(),
//intro: false, //intro: false,
//intro_2: false, //intro_2: false,
show: Show { show: Show {
@ -595,7 +617,7 @@ impl Hud {
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let energy = ecs.read_storage::<comp::Energy>(); let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>(); let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
let speech_bubbles = ecs.read_storage::<comp::SpeechBubble>(); let uids = ecs.read_storage::<common::sync::Uid>();
let interpolated = ecs.read_storage::<vcomp::Interpolated>(); let interpolated = ecs.read_storage::<vcomp::Interpolated>();
let players = ecs.read_storage::<comp::Player>(); let players = ecs.read_storage::<comp::Player>();
let scales = ecs.read_storage::<comp::Scale>(); let scales = ecs.read_storage::<comp::Scale>();
@ -923,12 +945,24 @@ impl Hud {
} }
} }
// Pop speech bubbles
let now = Instant::now();
self.speech_bubbles
.retain(|_uid, bubble| bubble.timeout > now);
// Push speech bubbles
for msg in self.new_messages.iter() {
if let Some((bubble, uid)) = msg.to_bubble() {
self.speech_bubbles.insert(uid, bubble);
}
}
let mut overhead_walker = self.ids.overheads.walk(); let mut overhead_walker = self.ids.overheads.walk();
let mut sct_walker = self.ids.scts.walk(); let mut sct_walker = self.ids.scts.walk();
let mut sct_bg_walker = self.ids.sct_bgs.walk(); let mut sct_bg_walker = self.ids.sct_bgs.walk();
// Render overhead name tags and health bars // Render overhead name tags and health bars
for (pos, name, stats, energy, height_offset, hpfl, bubble) in ( for (pos, name, stats, energy, height_offset, hpfl, uid) in (
&entities, &entities,
&pos, &pos,
interpolated.maybe(), interpolated.maybe(),
@ -938,7 +972,7 @@ impl Hud {
scales.maybe(), scales.maybe(),
&bodies, &bodies,
&hp_floater_lists, &hp_floater_lists,
speech_bubbles.maybe(), &uids,
) )
.join() .join()
.filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead) .filter(|(entity, _, _, stats, _, _, _, _, _, _)| *entity != me && !stats.is_dead)
@ -955,7 +989,7 @@ impl Hud {
}) })
.powi(2) .powi(2)
}) })
.map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, bubble)| { .map(|(_, pos, interpolated, stats, energy, player, scale, body, hpfl, uid)| {
// TODO: This is temporary // TODO: This is temporary
// If the player used the default character name display their name instead // If the player used the default character name display their name instead
let name = if stats.name == "Character Name" { let name = if stats.name == "Character Name" {
@ -971,10 +1005,12 @@ impl Hud {
// TODO: when body.height() is more accurate remove the 2.0 // TODO: when body.height() is more accurate remove the 2.0
body.height() * 2.0 * scale.map_or(1.0, |s| s.0), body.height() * 2.0 * scale.map_or(1.0, |s| s.0),
hpfl, hpfl,
bubble, uid,
) )
}) })
{ {
let bubble = self.speech_bubbles.get(uid);
let overhead_id = overhead_walker.next( let overhead_id = overhead_walker.next(
&mut self.ids.overheads, &mut self.ids.overheads,
&mut ui_widgets.widget_id_generator(), &mut ui_widgets.widget_id_generator(),
@ -1440,11 +1476,11 @@ impl Hud {
} }
} }
// Popup // Popup (waypoint saved and similar notifications)
Popup::new( Popup::new(
&self.voxygen_i18n, &self.voxygen_i18n,
client, client,
&self.new_messages, &self.new_notifications,
&self.fonts, &self.fonts,
&self.show, &self.show,
) )
@ -1540,26 +1576,21 @@ impl Hud {
.set(self.ids.skillbar, ui_widgets); .set(self.ids.skillbar, ui_widgets);
} }
// The chat box breaks if it has non-chat messages left in the queue, so take // Don't put NPC messages in chat box.
// them out. self.new_messages
self.new_messages.retain(|msg| { .retain(|m| !matches!(m.chat_type, comp::ChatType::Npc(_, _)));
if let ClientEvent::Chat { .. } = &msg {
true
} else {
false
}
});
// Chat box // Chat box
match Chat::new( match Chat::new(
&mut self.new_messages, &mut self.new_messages,
&client,
global_state, global_state,
&self.imgs, &self.imgs,
&self.fonts, &self.fonts,
) )
.and_then(self.force_chat_input.take(), |c, input| c.input(input)) .and_then(self.force_chat_input.take(), |c, input| c.input(input))
.and_then(self.tab_complete.take(), |c, input| { .and_then(self.tab_complete.take(), |c, input| {
c.prepare_tab_completion(input, &client) c.prepare_tab_completion(input)
}) })
.and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos)) .and_then(self.force_chat_cursor.take(), |c, pos| c.cursor_pos(pos))
.set(self.ids.chat, ui_widgets) .set(self.ids.chat, ui_widgets)
@ -1577,6 +1608,7 @@ impl Hud {
} }
self.new_messages = VecDeque::new(); self.new_messages = VecDeque::new();
self.new_notifications = VecDeque::new();
// Windows // Windows
@ -1599,6 +1631,9 @@ impl Hud {
settings_window::Event::SpeechBubbleDarkMode(sbdm) => { settings_window::Event::SpeechBubbleDarkMode(sbdm) => {
events.push(Event::SpeechBubbleDarkMode(sbdm)); events.push(Event::SpeechBubbleDarkMode(sbdm));
}, },
settings_window::Event::SpeechBubbleIcon(sbi) => {
events.push(Event::SpeechBubbleIcon(sbi));
},
settings_window::Event::Sct(sct) => { settings_window::Event::Sct(sct) => {
events.push(Event::Sct(sct)); events.push(Event::Sct(sct));
}, },
@ -1628,6 +1663,9 @@ impl Hud {
settings_window::Event::ChatTransp(chat_transp) => { settings_window::Event::ChatTransp(chat_transp) => {
events.push(Event::ChatTransp(chat_transp)); events.push(Event::ChatTransp(chat_transp));
}, },
settings_window::Event::ChatCharName(chat_char_name) => {
events.push(Event::ChatCharName(chat_char_name));
},
settings_window::Event::ToggleZoomInvert(zoom_inverted) => { settings_window::Event::ToggleZoomInvert(zoom_inverted) => {
events.push(Event::ToggleZoomInvert(zoom_inverted)); events.push(Event::ToggleZoomInvert(zoom_inverted));
}, },
@ -1903,7 +1941,11 @@ impl Hud {
events events
} }
pub fn new_message(&mut self, msg: ClientEvent) { self.new_messages.push_back(msg); } pub fn new_message(&mut self, msg: comp::ChatMsg) { self.new_messages.push_back(msg); }
pub fn new_notification(&mut self, msg: common::msg::Notification) {
self.new_notifications.push_back(msg);
}
pub fn scale_change(&mut self, scale_change: ScaleChange) -> ScaleMode { pub fn scale_change(&mut self, scale_change: ScaleChange) -> ScaleMode {
let scale_mode = match scale_change { let scale_mode = match scale_change {

View File

@ -1,21 +1,26 @@
use super::{img_ids::Imgs, HP_COLOR, LOW_HP_COLOR, MANA_COLOR}; use super::{
img_ids::Imgs, FACTION_COLOR, GROUP_COLOR, HP_COLOR, LOW_HP_COLOR, MANA_COLOR, REGION_COLOR,
SAY_COLOR, TELL_COLOR, TEXT_BG, TEXT_COLOR,
};
use crate::{ use crate::{
i18n::VoxygenLocalization, i18n::VoxygenLocalization,
settings::GameplaySettings, settings::GameplaySettings,
ui::{fonts::ConrodVoxygenFonts, Ingameable}, ui::{fonts::ConrodVoxygenFonts, Ingameable},
}; };
use common::comp::{Energy, SpeechBubble, Stats}; use common::comp::{Energy, SpeechBubble, SpeechBubbleType, Stats};
use conrod_core::{ use conrod_core::{
position::Align, position::Align,
widget::{self, Image, Rectangle, Text}, widget::{self, Image, Rectangle, Text},
widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon, widget_ids, Color, Colorable, Positionable, Sizeable, Widget, WidgetCommon,
}; };
const MAX_BUBBLE_WIDTH: f64 = 250.0;
widget_ids! { widget_ids! {
struct Ids { struct Ids {
// Speech bubble // Speech bubble
speech_bubble_text, speech_bubble_text,
speech_bubble_text2, speech_bubble_shadow,
speech_bubble_top_left, speech_bubble_top_left,
speech_bubble_top, speech_bubble_top,
speech_bubble_top_right, speech_bubble_top_right,
@ -26,6 +31,7 @@ widget_ids! {
speech_bubble_bottom, speech_bubble_bottom,
speech_bubble_bottom_right, speech_bubble_bottom_right,
speech_bubble_tail, speech_bubble_tail,
speech_bubble_icon,
// Name // Name
name_bg, name_bg,
@ -101,9 +107,10 @@ impl<'a> Ingameable for Overhead<'a> {
// - 1 for level: either Text or Image // - 1 for level: either Text or Image
// - 4 for HP + mana + fg + bg // - 4 for HP + mana + fg + bg
// If there's a speech bubble // If there's a speech bubble
// - 1 Text::new for speech bubble // - 2 Text::new for speech bubble
// - 1 Image::new for icon
// - 10 Image::new for speech bubble (9-slice + tail) // - 10 Image::new for speech bubble (9-slice + tail)
7 + if self.bubble.is_some() { 11 } else { 0 } 7 + if self.bubble.is_some() { 13 } else { 0 }
} }
} }
@ -146,23 +153,20 @@ impl<'a> Widget for Overhead<'a> {
if let Some(bubble) = self.bubble { if let Some(bubble) = self.bubble {
let dark_mode = self.settings.speech_bubble_dark_mode; let dark_mode = self.settings.speech_bubble_dark_mode;
let localizer = let localizer =
|s: String, i| -> String { self.voxygen_i18n.get_variation(&s, i).to_string() }; |s: &str, i| -> String { self.voxygen_i18n.get_variation(&s, i).to_string() };
let bubble_contents: String = bubble.message(localizer); let bubble_contents: String = bubble.message(localizer);
let (text_color, shadow_color) = bubble_color(&bubble, dark_mode);
let mut text = Text::new(&bubble_contents) let mut text = Text::new(&bubble_contents)
.color(text_color)
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.font_size(18) .font_size(18)
.up_from(state.ids.name, 20.0) .up_from(state.ids.name, 20.0)
.x_align_to(state.ids.name, Align::Middle) .x_align_to(state.ids.name, Align::Middle)
.parent(id); .parent(id);
text = if dark_mode {
text.color(Color::Rgba(1.0, 1.0, 1.0, 1.0))
} else {
text.color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
};
if let Some(w) = text.get_w(ui) { if let Some(w) = text.get_w(ui) {
if w > 250.0 { if w > MAX_BUBBLE_WIDTH {
text = text.w(250.0); text = text.w(MAX_BUBBLE_WIDTH);
} }
} }
Image::new(if dark_mode { Image::new(if dark_mode {
@ -255,13 +259,40 @@ impl<'a> Widget for Overhead<'a> {
} else { } else {
self.imgs.speech_bubble_tail self.imgs.speech_bubble_tail
}) })
.w_h(22.0, 28.0) .parent(id)
.mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0) .mid_bottom_with_margin_on(state.ids.speech_bubble_text, -32.0);
.parent(id);
if dark_mode {
tail.w_h(22.0, 13.0).set(state.ids.speech_bubble_tail, ui)
} else {
tail.w_h(22.0, 28.0).set(state.ids.speech_bubble_tail, ui)
};
let mut text_shadow = Text::new(&bubble_contents)
.color(shadow_color)
.font_id(self.fonts.cyri.conrod_id)
.font_size(18)
.x_relative_to(state.ids.speech_bubble_text, 1.0)
.y_relative_to(state.ids.speech_bubble_text, -1.0)
.parent(id);
// Move text to front (conrod depth is lowest first; not a z-index) // Move text to front (conrod depth is lowest first; not a z-index)
tail.set(state.ids.speech_bubble_tail, ui); text.depth(text_shadow.get_depth() - 1.0)
text.depth(tail.get_depth() - 1.0)
.set(state.ids.speech_bubble_text, ui); .set(state.ids.speech_bubble_text, ui);
if let Some(w) = text_shadow.get_w(ui) {
if w > MAX_BUBBLE_WIDTH {
text_shadow = text_shadow.w(MAX_BUBBLE_WIDTH);
}
}
text_shadow.set(state.ids.speech_bubble_shadow, ui);
let icon = if self.settings.speech_bubble_icon {
bubble_icon(&bubble, &self.imgs)
} else {
self.imgs.nothing
};
Image::new(icon)
.w_h(16.0, 16.0)
.top_left_with_margin_on(state.ids.speech_bubble_text, -16.0)
.set(state.ids.speech_bubble_icon, ui);
} }
let hp_percentage = let hp_percentage =
@ -361,3 +392,37 @@ impl<'a> Widget for Overhead<'a> {
} }
} }
} }
fn bubble_color(bubble: &SpeechBubble, dark_mode: bool) -> (Color, Color) {
let light_color = match bubble.icon {
SpeechBubbleType::Tell => TELL_COLOR,
SpeechBubbleType::Say => SAY_COLOR,
SpeechBubbleType::Region => REGION_COLOR,
SpeechBubbleType::Group => GROUP_COLOR,
SpeechBubbleType::Faction => FACTION_COLOR,
SpeechBubbleType::World
| SpeechBubbleType::Quest
| SpeechBubbleType::Trade
| SpeechBubbleType::None => TEXT_COLOR,
};
if dark_mode {
(light_color, TEXT_BG)
} else {
(TEXT_BG, light_color)
}
}
fn bubble_icon(sb: &SpeechBubble, imgs: &Imgs) -> conrod_core::image::Id {
match sb.icon {
// One for each chat mode
SpeechBubbleType::Tell => imgs.chat_tell_small,
SpeechBubbleType::Say => imgs.chat_say_small,
SpeechBubbleType::Region => imgs.chat_region_small,
SpeechBubbleType::Group => imgs.chat_group_small,
SpeechBubbleType::Faction => imgs.chat_faction_small,
SpeechBubbleType::World => imgs.chat_world_small,
SpeechBubbleType::Quest => imgs.nothing, // TODO not implemented
SpeechBubbleType::Trade => imgs.nothing, // TODO not implemented
SpeechBubbleType::None => imgs.nothing, // No icon (default for npcs)
}
}

View File

@ -1,6 +1,6 @@
use super::Show; use super::Show;
use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts}; use crate::{i18n::VoxygenLocalization, ui::fonts::ConrodVoxygenFonts};
use client::{self, Client, Event as ClientEvent}; use client::{self, Client};
use common::msg::Notification; use common::msg::Notification;
use conrod_core::{ use conrod_core::{
widget::{self, Text}, widget::{self, Text},
@ -23,7 +23,7 @@ widget_ids! {
pub struct Popup<'a> { pub struct Popup<'a> {
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>, voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
client: &'a Client, client: &'a Client,
new_messages: &'a VecDeque<ClientEvent>, new_notifications: &'a VecDeque<Notification>,
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
#[conrod(common_builder)] #[conrod(common_builder)]
common: widget::CommonBuilder, common: widget::CommonBuilder,
@ -36,14 +36,14 @@ impl<'a> Popup<'a> {
pub fn new( pub fn new(
voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>, voxygen_i18n: &'a std::sync::Arc<VoxygenLocalization>,
client: &'a Client, client: &'a Client,
new_messages: &'a VecDeque<ClientEvent>, new_notifications: &'a VecDeque<Notification>,
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
show: &'a Show, show: &'a Show,
) -> Self { ) -> Self {
Self { Self {
voxygen_i18n, voxygen_i18n,
client, client,
new_messages, new_notifications,
fonts, fonts,
common: widget::CommonBuilder::default(), common: widget::CommonBuilder::default(),
show, show,
@ -119,9 +119,9 @@ impl<'a> Widget for Popup<'a> {
} }
// Push waypoint to message queue // Push waypoint to message queue
for notification in self.new_messages { for notification in self.new_notifications {
match notification { match notification {
ClientEvent::Notification(Notification::WaypointSaved) => { Notification::WaypointSaved => {
state.update(|s| { state.update(|s| {
if s.infos.is_empty() { if s.infos.is_empty() {
s.last_info_update = Instant::now(); s.last_info_update = Instant::now();
@ -130,7 +130,6 @@ impl<'a> Widget for Popup<'a> {
s.infos.push_back(text.to_string()); s.infos.push_back(text.to_string());
}); });
}, },
_ => {},
} }
} }

View File

@ -11,7 +11,7 @@ use crate::{
}; };
use conrod_core::{ use conrod_core::{
color, color,
position::Relative, position::{Align, Relative},
widget::{self, Button, DropDownList, Image, Rectangle, Scrollbar, Text}, widget::{self, Button, DropDownList, Image, Rectangle, Scrollbar, Text},
widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget, widget_ids, Borderable, Color, Colorable, Labelable, Positionable, Sizeable, Widget,
WidgetCommon, WidgetCommon,
@ -137,6 +137,8 @@ widget_ids! {
chat_transp_title, chat_transp_title,
chat_transp_text, chat_transp_text,
chat_transp_slider, chat_transp_slider,
chat_char_name_text,
chat_char_name_button,
sct_title, sct_title,
sct_show_text, sct_show_text,
sct_show_radio, sct_show_radio,
@ -153,8 +155,11 @@ widget_ids! {
sct_num_dur_text, sct_num_dur_text,
sct_num_dur_slider, sct_num_dur_slider,
sct_num_dur_value, sct_num_dur_value,
speech_bubble_text,
speech_bubble_dark_mode_text, speech_bubble_dark_mode_text,
speech_bubble_dark_mode_button, speech_bubble_dark_mode_button,
speech_bubble_icon_text,
speech_bubble_icon_button,
free_look_behavior_text, free_look_behavior_text,
free_look_behavior_list, free_look_behavior_list,
auto_walk_behavior_text, auto_walk_behavior_text,
@ -237,10 +242,12 @@ pub enum Event {
CrosshairType(CrosshairType), CrosshairType(CrosshairType),
UiScale(ScaleChange), UiScale(ScaleChange),
ChatTransp(f32), ChatTransp(f32),
ChatCharName(bool),
Sct(bool), Sct(bool),
SctPlayerBatch(bool), SctPlayerBatch(bool),
SctDamageBatch(bool), SctDamageBatch(bool),
SpeechBubbleDarkMode(bool), SpeechBubbleDarkMode(bool),
SpeechBubbleIcon(bool),
ChangeLanguage(LanguageMetadata), ChangeLanguage(LanguageMetadata),
ChangeBinding(GameInput), ChangeBinding(GameInput),
ChangeFreeLookBehavior(PressBehavior), ChangeFreeLookBehavior(PressBehavior),
@ -926,20 +933,27 @@ impl<'a> Widget for SettingsWindow<'a> {
} }
// Speech bubble dark mode // Speech bubble dark mode
Text::new(&self.localized_strings.get("hud.settings.speech_bubble"))
.down_from(
if self.global_state.settings.gameplay.sct {
state.ids.sct_batch_inc_radio
} else {
state.ids.sct_show_radio
},
20.0,
)
.x_align(Align::Start)
.x_relative_to(state.ids.sct_show_text, -40.0)
.font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.speech_bubble_text, ui);
let speech_bubble_dark_mode = ToggleButton::new( let speech_bubble_dark_mode = ToggleButton::new(
self.global_state.settings.gameplay.speech_bubble_dark_mode, self.global_state.settings.gameplay.speech_bubble_dark_mode,
self.imgs.checkbox, self.imgs.checkbox,
self.imgs.checkbox_checked, self.imgs.checkbox_checked,
) )
.down_from( .down_from(state.ids.speech_bubble_text, 10.0)
if self.global_state.settings.gameplay.sct {
state.ids.sct_batch_inc_radio
} else {
state.ids.sct_show_radio
},
20.0,
)
.x(0.0)
.w_h(18.0, 18.0) .w_h(18.0, 18.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
@ -955,15 +969,39 @@ impl<'a> Widget for SettingsWindow<'a> {
.get("hud.settings.speech_bubble_dark_mode"), .get("hud.settings.speech_bubble_dark_mode"),
) )
.right_from(state.ids.speech_bubble_dark_mode_button, 10.0) .right_from(state.ids.speech_bubble_dark_mode_button, 10.0)
.font_size(self.fonts.cyri.scale(18)) .font_size(self.fonts.cyri.scale(15))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(state.ids.speech_bubble_dark_mode_text, ui); .set(state.ids.speech_bubble_dark_mode_text, ui);
// Speech bubble icon
let speech_bubble_icon = ToggleButton::new(
self.global_state.settings.gameplay.speech_bubble_icon,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.down_from(state.ids.speech_bubble_dark_mode_button, 10.0)
.w_h(18.0, 18.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.speech_bubble_icon_button, ui);
if self.global_state.settings.gameplay.speech_bubble_icon != speech_bubble_icon {
events.push(Event::SpeechBubbleIcon(speech_bubble_icon));
}
Text::new(
&self
.localized_strings
.get("hud.settings.speech_bubble_icon"),
)
.right_from(state.ids.speech_bubble_icon_button, 10.0)
.font_size(self.fonts.cyri.scale(15))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.speech_bubble_icon_text, ui);
// Energybars Numbers // Energybars Numbers
// Hotbar text // Hotbar text
Text::new(&self.localized_strings.get("hud.settings.energybar_numbers")) Text::new(&self.localized_strings.get("hud.settings.energybar_numbers"))
.down_from(state.ids.speech_bubble_dark_mode_button, 20.0) .down_from(state.ids.speech_bubble_icon_button, 20.0)
.font_size(self.fonts.cyri.scale(18)) .font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR) .color(TEXT_COLOR)
@ -1097,8 +1135,36 @@ impl<'a> Widget for SettingsWindow<'a> {
events.push(Event::ChatTransp(new_val)); events.push(Event::ChatTransp(new_val));
} }
// "Show character names in chat" toggle button
let chat_char_name = ToggleButton::new(
self.global_state.settings.gameplay.chat_character_name,
self.imgs.checkbox,
self.imgs.checkbox_checked,
)
.w_h(18.0, 18.0)
.down_from(state.ids.chat_transp_slider, 20.0)
.hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo)
.press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked)
.set(state.ids.chat_char_name_button, ui);
if self.global_state.settings.gameplay.chat_character_name != chat_char_name {
events.push(Event::ChatCharName(
!self.global_state.settings.gameplay.chat_character_name,
));
}
Text::new(
&self
.localized_strings
.get("hud.settings.chat_character_name"),
)
.right_from(state.ids.chat_char_name_button, 20.0)
.font_size(self.fonts.cyri.scale(14))
.font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR)
.set(state.ids.chat_char_name_text, ui);
// Language select drop down
Text::new(&self.localized_strings.get("common.languages")) Text::new(&self.localized_strings.get("common.languages"))
.down_from(state.ids.chat_transp_slider, 20.0) .down_from(state.ids.chat_char_name_button, 20.0)
.font_size(self.fonts.cyri.scale(18)) .font_size(self.fonts.cyri.scale(18))
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR) .color(TEXT_COLOR)

View File

@ -175,7 +175,8 @@ impl<'a> Widget for Social<'a> {
// Players list // Players list
// TODO: this list changes infrequently enough that it should not have to be // TODO: this list changes infrequently enough that it should not have to be
// recreated every frame // recreated every frame
let count = self.client.player_list.len(); let players = self.client.player_list.values().filter(|p| p.is_online);
let count = players.clone().count();
if ids.player_names.len() < count { if ids.player_names.len() < count {
ids.update(|ids| { ids.update(|ids| {
ids.player_names ids.player_names
@ -193,7 +194,7 @@ impl<'a> Widget for Social<'a> {
.font_id(self.fonts.cyri.conrod_id) .font_id(self.fonts.cyri.conrod_id)
.color(TEXT_COLOR) .color(TEXT_COLOR)
.set(ids.online_title, ui); .set(ids.online_title, ui);
for (i, (_, player_info)) in self.client.player_list.iter().enumerate() { for (i, player_info) in players.enumerate() {
Text::new(&format!( Text::new(&format!(
"[{}] {}", "[{}] {}",
player_info.player_alias, player_info.player_alias,

View File

@ -9,17 +9,16 @@ use crate::{
window::{AnalogGameInput, Event, GameInput}, window::{AnalogGameInput, Event, GameInput},
Direction, Error, GlobalState, PlayState, PlayStateResult, Direction, Error, GlobalState, PlayState, PlayStateResult,
}; };
use client::{self, Client, Event::Chat}; use client::{self, Client};
use common::{ use common::{
assets::{load_watched, watch}, assets::{load_watched, watch},
clock::Clock, clock::Clock,
comp, comp,
comp::{Pos, Vel, MAX_PICKUP_RANGE_SQR}, comp::{Pos, Vel, MAX_PICKUP_RANGE_SQR},
msg::{ClientState, Notification}, msg::ClientState,
terrain::{Block, BlockKind}, terrain::{Block, BlockKind},
util::Dir, util::Dir,
vol::ReadVol, vol::ReadVol,
ChatType,
}; };
use specs::{Join, WorldExt}; use specs::{Join, WorldExt};
use std::{cell::RefCell, rc::Rc, time::Duration}; use std::{cell::RefCell, rc::Rc, time::Duration};
@ -78,18 +77,11 @@ impl SessionState {
fn tick(&mut self, dt: Duration, global_state: &mut GlobalState) -> Result<TickAction, Error> { fn tick(&mut self, dt: Duration, global_state: &mut GlobalState) -> Result<TickAction, Error> {
self.inputs.tick(dt); self.inputs.tick(dt);
for event in self.client.borrow_mut().tick( let mut client = self.client.borrow_mut();
self.inputs.clone(), for event in client.tick(self.inputs.clone(), dt, crate::ecs::sys::add_local_systems)? {
dt,
crate::ecs::sys::add_local_systems,
)? {
match event { match event {
Chat { client::Event::Chat(m) => {
chat_type: _, self.hud.new_message(m);
ref message,
} => {
info!("[CHAT] {}", message);
self.hud.new_message(event);
}, },
client::Event::Disconnect => return Ok(TickAction::Disconnect), client::Event::Disconnect => return Ok(TickAction::Disconnect),
client::Event::DisconnectionNotification(time) => { client::Event::DisconnectionNotification(time) => {
@ -98,14 +90,13 @@ impl SessionState {
_ => format!("Connection lost. Kicking in {} seconds", time), _ => format!("Connection lost. Kicking in {} seconds", time),
}; };
self.hud.new_message(Chat { self.hud.new_message(comp::ChatMsg {
chat_type: ChatType::Meta, chat_type: comp::ChatType::CommandError,
message, message,
}); });
}, },
client::Event::Notification(Notification::WaypointSaved) => { client::Event::Notification(n) => {
self.hud self.hud.new_notification(n);
.new_message(client::Event::Notification(Notification::WaypointSaved));
}, },
client::Event::SetViewDistance(vd) => { client::Event::SetViewDistance(vd) => {
global_state.settings.graphics.view_distance = vd; global_state.settings.graphics.view_distance = vd;
@ -507,10 +498,12 @@ impl PlayState for SessionState {
self.scene.handle_input_event(Event::AnalogGameInput(other)); self.scene.handle_input_event(Event::AnalogGameInput(other));
}, },
}, },
Event::ScreenshotMessage(screenshot_message) => self.hud.new_message(Chat { Event::ScreenshotMessage(screenshot_message) => {
chat_type: ChatType::Meta, self.hud.new_message(comp::ChatMsg {
message: screenshot_message, chat_type: comp::ChatType::CommandInfo,
}), message: screenshot_message,
})
},
// Pass all other events to the scene // Pass all other events to the scene
event => { event => {
@ -656,6 +649,10 @@ impl PlayState for SessionState {
global_state.settings.gameplay.speech_bubble_dark_mode = sbdm; global_state.settings.gameplay.speech_bubble_dark_mode = sbdm;
global_state.settings.save_to_file_warn(); global_state.settings.save_to_file_warn();
}, },
HudEvent::SpeechBubbleIcon(sbi) => {
global_state.settings.gameplay.speech_bubble_icon = sbi;
global_state.settings.save_to_file_warn();
},
HudEvent::ToggleDebug(toggle_debug) => { HudEvent::ToggleDebug(toggle_debug) => {
global_state.settings.gameplay.toggle_debug = toggle_debug; global_state.settings.gameplay.toggle_debug = toggle_debug;
global_state.settings.save_to_file_warn(); global_state.settings.save_to_file_warn();
@ -693,6 +690,10 @@ impl PlayState for SessionState {
global_state.settings.gameplay.chat_transp = chat_transp; global_state.settings.gameplay.chat_transp = chat_transp;
global_state.settings.save_to_file_warn(); global_state.settings.save_to_file_warn();
}, },
HudEvent::ChatCharName(chat_char_name) => {
global_state.settings.gameplay.chat_character_name = chat_char_name;
global_state.settings.save_to_file_warn();
},
HudEvent::CrosshairType(crosshair_type) => { HudEvent::CrosshairType(crosshair_type) => {
global_state.settings.gameplay.crosshair_type = crosshair_type; global_state.settings.gameplay.crosshair_type = crosshair_type;
global_state.settings.save_to_file_warn(); global_state.settings.save_to_file_warn();

View File

@ -458,10 +458,12 @@ pub struct GameplaySettings {
pub sct_player_batch: bool, pub sct_player_batch: bool,
pub sct_damage_batch: bool, pub sct_damage_batch: bool,
pub speech_bubble_dark_mode: bool, pub speech_bubble_dark_mode: bool,
pub speech_bubble_icon: bool,
pub mouse_y_inversion: bool, pub mouse_y_inversion: bool,
pub smooth_pan_enable: bool, pub smooth_pan_enable: bool,
pub crosshair_transp: f32, pub crosshair_transp: f32,
pub chat_transp: f32, pub chat_transp: f32,
pub chat_character_name: bool,
pub crosshair_type: CrosshairType, pub crosshair_type: CrosshairType,
pub intro_show: Intro, pub intro_show: Intro,
pub xp_bar: XpBar, pub xp_bar: XpBar,
@ -486,8 +488,10 @@ impl Default for GameplaySettings {
sct_player_batch: true, sct_player_batch: true,
sct_damage_batch: false, sct_damage_batch: false,
speech_bubble_dark_mode: false, speech_bubble_dark_mode: false,
speech_bubble_icon: true,
crosshair_transp: 0.6, crosshair_transp: 0.6,
chat_transp: 0.4, chat_transp: 0.4,
chat_character_name: true,
crosshair_type: CrosshairType::Round, crosshair_type: CrosshairType::Round,
intro_show: Intro::Show, intro_show: Intro::Show,
xp_bar: XpBar::Always, xp_bar: XpBar::Always,