mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'CapsizeGlimmer/chat_modes' into 'master'
Capsize glimmer/chat modes Closes #217 See merge request veloren/veloren!1043
This commit is contained in:
commit
9246e34c1b
@ -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
BIN
assets/voxygen/element/frames/bubble_dark/icon_frame.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
assets/voxygen/element/frames/bubble_dark/tail.png
(Stored with Git LFS)
BIN
assets/voxygen/element/frames/bubble_dark/tail.png
(Stored with Git LFS)
Binary file not shown.
BIN
assets/voxygen/element/icons/chat/command_error_small.png
(Stored with Git LFS)
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
BIN
assets/voxygen/element/icons/chat/world_small.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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}
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
@ -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
260
common/src/comp/chat.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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> {
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
|
@ -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,
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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 {
|
||||||
|
@ -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
@ -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,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)),
|
)),
|
||||||
|
@ -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()
|
||||||
|
@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.");
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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, &[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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: (),
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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());
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_ => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user