* Added new Admin Commands window to egui, including Give Items and Kits sections

* Added widgets.rs to egui for reusable widgets
* Added filterable_list egui widget
* Reworked DebugShapeAction to be a more generic EguiAction which now allows for ChatCommands (used by admin tools) as well as DebugShape drawing requests.
* Fixed egui event handling so that typing/clicking within egui windows now correctly doesn't pass these events onto the game itself
* Removed /give_item limit for stackable items
This commit is contained in:
Ben Wallis 2021-08-21 10:23:26 +01:00
parent 2f1fe39e04
commit d665ce329d
10 changed files with 267 additions and 76 deletions

View File

@ -255,9 +255,11 @@ lazy_static! {
})
};
static ref KITS: Vec<String> = {
pub static ref KITS: Vec<String> = {
if let Ok(kits) = KitManifest::load(KIT_MANIFEST_PATH) {
kits.read().0.keys().cloned().collect()
let mut kits = kits.read().0.keys().cloned().collect::<Vec<String>>();
kits.sort();
kits
} else {
Vec::new()
}

View File

@ -472,7 +472,16 @@ fn handle_give_item(
if let Ok(item) = Item::new_from_asset(&item_name.replace('/', ".").replace("\\", ".")) {
let mut item: Item = item;
let mut res = Ok(());
if let Ok(()) = item.set_amount(give_amount.min(2000)) {
const MAX_GIVE_AMOUNT: u32 = 2000;
// Cap give_amount for non-stackable items
let give_amount = if item.is_stackable() {
give_amount
} else {
give_amount.min(MAX_GIVE_AMOUNT)
};
if let Ok(()) = item.set_amount(give_amount) {
server
.state
.ecs()

View File

@ -5,7 +5,7 @@ edition = "2018"
version = "0.9.0"
[features]
use-dyn-lib = ["lazy_static", "voxygen-dynlib"]
use-dyn-lib = ["voxygen-dynlib"]
be-dyn-lib = []
[dependencies]
@ -13,8 +13,7 @@ client = {package = "veloren-client", path = "../../client"}
common = {package = "veloren-common", path = "../../common"}
egui = "0.12"
egui_winit_platform = "0.8"
lazy_static = "1.4.0"
voxygen-dynlib = {package = "veloren-voxygen-dynlib", path = "../dynlib", optional = true}
# Hot Reloading
lazy_static = {version = "1.4.0", optional = true}

94
voxygen/egui/src/admin.rs Normal file
View File

@ -0,0 +1,94 @@
use crate::{AdminCommandState, EguiAction, EguiActions, EguiWindows};
use common::cmd::ChatCommand;
use egui::{CollapsingHeader, CtxRef, Resize, Slider, Ui, Vec2, Window};
use lazy_static::lazy_static;
lazy_static! {
static ref ITEM_SPECS: Vec<String> = {
let mut item_specs = common::cmd::ITEM_SPECS
.iter()
.map(|item_desc| item_desc.replace("common.items.", ""))
.collect::<Vec<String>>();
item_specs.sort();
item_specs
};
}
pub fn draw_admin_commands_window(
ctx: &CtxRef,
state: &mut AdminCommandState,
windows: &mut EguiWindows,
egui_actions: &mut EguiActions,
) {
Window::new("Admin Commands")
.open(&mut windows.admin_commands)
.default_width(400.0)
.default_height(600.0)
.show(ctx, |ui| {
ui.spacing_mut().item_spacing = Vec2::new(10.0, 10.0);
ui.vertical(|ui| {
CollapsingHeader::new("Give Items")
.default_open(true)
.show(ui, |ui| {
draw_give_items(ui, state, egui_actions);
});
CollapsingHeader::new("Kits")
.default_open(false)
.show(ui, |ui| {
draw_kits(ui, state, egui_actions);
});
});
});
}
fn draw_kits(ui: &mut Ui, state: &mut AdminCommandState, egui_actions: &mut EguiActions) {
ui.vertical(|ui| {
if ui.button("Give Kit").clicked() {
egui_actions.actions.push(EguiAction::ChatCommand {
cmd: ChatCommand::Kit,
args: vec![common::cmd::KITS[state.kits_selected_idx].clone()],
});
};
crate::widgets::filterable_list(ui, &common::cmd::KITS, "", &mut state.kits_selected_idx)
});
}
fn draw_give_items(ui: &mut Ui, state: &mut AdminCommandState, egui_actions: &mut EguiActions) {
ui.spacing_mut().window_padding = Vec2::new(10.0, 10.0);
Resize::default()
.default_size([400.0, 200.0])
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.add(
Slider::new(&mut state.give_item_qty, 1..=100000)
.logarithmic(true)
.clamp_to_range(true)
.text("Qty"),
);
if ui.button("Give Items").clicked() {
egui_actions.actions.push(EguiAction::ChatCommand {
cmd: ChatCommand::GiveItem,
args: vec![
format!(
"common.items.{}",
ITEM_SPECS[state.give_item_selected_idx].clone()
),
format!("{}", state.give_item_qty),
],
});
};
});
ui.horizontal(|ui| {
ui.label("Filter:");
ui.text_edit_singleline(&mut state.give_item_search_text);
});
crate::widgets::filterable_list(
ui,
&ITEM_SPECS,
&state.give_item_search_text,
&mut state.give_item_selected_idx,
);
});
}

View File

@ -1,4 +1,4 @@
use crate::{two_col_row, SelectedEntityInfo};
use crate::{widgets::two_col_row, SelectedEntityInfo};
use common::{
comp::CharacterState,
states::{charged_melee, combo_melee, dash_melee, leap_melee},

View File

@ -3,7 +3,9 @@
#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
mod admin;
mod character_states;
mod widgets;
use client::{Client, Join, World, WorldExt};
use common::{
@ -14,17 +16,17 @@ use core::mem;
use egui::{
plot::{Plot, Value},
widgets::plot::Curve,
CollapsingHeader, Color32, Grid, Label, Pos2, ScrollArea, Slider, Ui, Window,
CollapsingHeader, Color32, Grid, Pos2, ScrollArea, Slider, Ui, Window,
};
fn two_col_row(ui: &mut Ui, label: impl Into<Label>, content: impl Into<Label>) {
ui.label(label);
ui.label(content);
ui.end_row();
}
use crate::character_states::draw_char_state_group;
use common::comp::{aura::AuraKind::Buff, Body, Fluid};
use crate::{
admin::draw_admin_commands_window, character_states::draw_char_state_group,
widgets::two_col_row,
};
use common::{
cmd::ChatCommand,
comp::{aura::AuraKind::Buff, Body, Fluid},
};
use egui_winit_platform::Platform;
use std::time::Duration;
#[cfg(feature = "use-dyn-lib")]
@ -58,6 +60,24 @@ impl SelectedEntityInfo {
}
}
pub struct AdminCommandState {
give_item_qty: u32,
give_item_selected_idx: usize,
give_item_search_text: String,
kits_selected_idx: usize,
}
impl AdminCommandState {
fn new() -> Self {
Self {
give_item_qty: 1,
give_item_selected_idx: 0,
give_item_search_text: String::new(),
kits_selected_idx: 0,
}
}
}
pub struct EguiDebugInfo {
pub frame_time: Duration,
pub ping_ms: f64,
@ -65,13 +85,16 @@ pub struct EguiDebugInfo {
pub struct EguiInnerState {
selected_entity_info: Option<SelectedEntityInfo>,
admin_command_state: AdminCommandState,
max_entity_distance: f32,
selected_entity_cylinder_height: f32,
frame_times: Vec<f32>,
windows: EguiWindows,
}
#[derive(Clone, Default)]
pub struct EguiWindows {
admin_commands: bool,
egui_inspection: bool,
egui_settings: bool,
egui_memory: bool,
@ -82,15 +105,17 @@ pub struct EguiWindows {
impl Default for EguiInnerState {
fn default() -> Self {
Self {
admin_command_state: AdminCommandState::new(),
selected_entity_info: None,
max_entity_distance: 100000.0,
selected_entity_cylinder_height: 10.0,
frame_times: Vec::new(),
windows: EguiWindows::default(),
}
}
}
pub enum DebugShapeAction {
pub enum EguiDebugShapeAction {
AddCylinder {
radius: f32,
height: f32,
@ -103,9 +128,14 @@ pub enum DebugShapeAction {
},
}
pub enum EguiAction {
ChatCommand { cmd: ChatCommand, args: Vec<String> },
DebugShape(EguiDebugShapeAction),
}
#[derive(Default)]
pub struct EguiActions {
pub actions: Vec<DebugShapeAction>,
pub actions: Vec<EguiAction>,
}
#[cfg(feature = "use-dyn-lib")]
@ -114,7 +144,6 @@ pub fn init() { lazy_static::initialize(&LIB); }
pub fn maintain(
platform: &mut Platform,
egui_state: &mut EguiInnerState,
egui_windows: &mut EguiWindows,
client: &Client,
debug_info: Option<EguiDebugInfo>,
added_cylinder_shape_id: Option<u64>,
@ -124,7 +153,6 @@ pub fn maintain(
maintain_egui_inner(
platform,
egui_state,
egui_windows,
client,
debug_info,
added_cylinder_shape_id,
@ -141,7 +169,6 @@ pub fn maintain(
fn(
&mut Platform,
&mut EguiInnerState,
&mut EguiWindows,
&Client,
Option<EguiDebugInfo>,
Option<u64>,
@ -160,7 +187,6 @@ pub fn maintain(
maintain_fn(
platform,
egui_state,
egui_windows,
client,
debug_info,
added_cylinder_shape_id,
@ -172,7 +198,6 @@ pub fn maintain(
pub fn maintain_egui_inner(
platform: &mut Platform,
egui_state: &mut EguiInnerState,
egui_windows: &mut EguiWindows,
client: &Client,
debug_info: Option<EguiDebugInfo>,
added_cylinder_shape_id: Option<u64>,
@ -184,6 +209,7 @@ pub fn maintain_egui_inner(
let mut previous_selected_entity: Option<SelectedEntityInfo> = None;
let mut max_entity_distance = egui_state.max_entity_distance;
let mut selected_entity_cylinder_height = egui_state.selected_entity_cylinder_height;
let mut windows = egui_state.windows.clone();
// If a debug cylinder was added in the last frame, store it against the
// selected entity
@ -216,8 +242,9 @@ pub fn maintain_egui_inner(
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.checkbox(&mut egui_windows.ecs_entities, "ECS Entities");
ui.checkbox(&mut egui_windows.frame_time, "Frame Time");
ui.checkbox(&mut windows.admin_commands, "Admin Commands");
ui.checkbox(&mut windows.ecs_entities, "ECS Entities");
ui.checkbox(&mut windows.frame_time, "Frame Time");
});
});
@ -225,36 +252,36 @@ pub fn maintain_egui_inner(
ui.vertical(|ui| {
ui.label("Show EGUI Windows");
ui.horizontal(|ui| {
ui.checkbox(&mut egui_windows.egui_inspection, "🔍 Inspection");
ui.checkbox(&mut egui_windows.egui_settings, "🔧 Settings");
ui.checkbox(&mut egui_windows.egui_memory, "📝 Memory");
ui.checkbox(&mut windows.egui_inspection, "🔍 Inspection");
ui.checkbox(&mut windows.egui_settings, "🔧 Settings");
ui.checkbox(&mut windows.egui_memory, "📝 Memory");
})
})
});
});
Window::new("🔧 Settings")
.open(&mut egui_windows.egui_settings)
.open(&mut windows.egui_settings)
.scroll(true)
.show(ctx, |ui| {
ctx.settings_ui(ui);
});
Window::new("🔍 Inspection")
.open(&mut egui_windows.egui_inspection)
.open(&mut windows.egui_inspection)
.scroll(true)
.show(ctx, |ui| {
ctx.inspection_ui(ui);
});
Window::new("📝 Memory")
.open(&mut egui_windows.egui_memory)
.open(&mut windows.egui_memory)
.resizable(false)
.show(ctx, |ui| {
ctx.memory_ui(ui);
});
Window::new("Frame Time")
.open(&mut egui_windows.frame_time)
.open(&mut windows.frame_time)
.default_width(200.0)
.default_height(200.0)
.show(ctx, |ui| {
@ -268,14 +295,14 @@ pub fn maintain_egui_inner(
ui.add(plot);
});
if egui_windows.ecs_entities {
if windows.ecs_entities {
let ecs = client.state().ecs();
let positions = client.state().ecs().read_storage::<comp::Pos>();
let client_pos = positions.get(client.entity());
egui::Window::new("ECS Entities")
.open(&mut egui_windows.ecs_entities)
.open(&mut windows.ecs_entities)
.default_width(500.0)
.default_height(500.0)
.show(ctx, |ui| {
@ -333,10 +360,12 @@ pub fn maintain_egui_inner(
mem::take(&mut egui_state.selected_entity_info);
if pos.is_some() {
egui_actions.actions.push(DebugShapeAction::AddCylinder {
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::AddCylinder {
radius: 1.0,
height: egui_state.selected_entity_cylinder_height,
});
},
));
}
egui_state.selected_entity_info =
Some(SelectedEntityInfo::new(entity.id()));
@ -403,11 +432,20 @@ pub fn maintain_egui_inner(
}
}
draw_admin_commands_window(
ctx,
&mut egui_state.admin_command_state,
&mut windows,
&mut egui_actions,
);
if let Some(previous) = previous_selected_entity {
if let Some(debug_shape_id) = previous.debug_shape_id {
egui_actions
.actions
.push(DebugShapeAction::RemoveShape(debug_shape_id));
.push(EguiAction::DebugShape(EguiDebugShapeAction::RemoveShape(
debug_shape_id,
)));
}
};
@ -416,19 +454,22 @@ pub fn maintain_egui_inner(
if (egui_state.selected_entity_cylinder_height - selected_entity_cylinder_height).abs()
> f32::EPSILON
{
egui_actions
.actions
.push(DebugShapeAction::RemoveShape(debug_shape_id));
egui_actions.actions.push(DebugShapeAction::AddCylinder {
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::RemoveShape(debug_shape_id),
));
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::AddCylinder {
radius: 1.0,
height: selected_entity_cylinder_height,
});
},
));
}
}
};
egui_state.max_entity_distance = max_entity_distance;
egui_state.selected_entity_cylinder_height = selected_entity_cylinder_height;
egui_state.windows = windows;
egui_actions
}
@ -481,11 +522,13 @@ fn selected_entity_window(
{
if let Some(pos) = pos {
if let Some(shape_id) = selected_entity_info.debug_shape_id {
egui_actions.actions.push(DebugShapeAction::SetPosAndColor {
egui_actions.actions.push(EguiAction::DebugShape(
EguiDebugShapeAction::SetPosAndColor {
id: shape_id,
color: [1.0, 1.0, 0.0, 0.5],
pos: [pos.0.x, pos.0.y, pos.0.z + 2.0, 0.0],
});
},
));
}
};

View File

@ -0,0 +1,34 @@
use egui::{Label, ScrollArea, Ui, Vec2};
pub(crate) fn filterable_list(
ui: &mut Ui,
list_items: &[String],
search_text: &str,
selected_index: &mut usize,
) {
let scroll_area = ScrollArea::auto_sized();
scroll_area.show(ui, |ui| {
ui.spacing_mut().item_spacing = Vec2::new(0.0, 2.0);
let search_text = search_text.to_lowercase();
for (i, list_item) in list_items.iter().enumerate().filter_map(|(i, list_item)| {
if search_text.is_empty() || list_item.to_lowercase().contains(&search_text) {
Some((i, list_item))
} else {
None
}
}) {
if ui
.selectable_label(i == *selected_index, list_item)
.clicked()
{
*selected_index = i;
};
}
});
}
pub(crate) fn two_col_row(ui: &mut Ui, label: impl Into<Label>, content: impl Into<Label>) {
ui.label(label);
ui.label(content);
ui.end_row();
}

View File

@ -32,7 +32,13 @@ pub fn run(mut global_state: GlobalState, event_loop: EventLoop) {
*control_flow = winit::event_loop::ControlFlow::Poll;
#[cfg(feature = "egui-ui")]
{
global_state.egui_state.platform.handle_event(&event);
if global_state.egui_state.platform.captures_event(&event) {
return;
}
}
// Get events for the ui.
if let Some(event) = ui::Event::try_from(&event, global_state.window.window()) {
global_state.window.send_event(Event::Ui(event));

View File

@ -1091,7 +1091,7 @@ impl PlayState for SessionState {
#[cfg(feature = "egui-ui")]
if global_state.settings.interface.egui_enabled() {
global_state.egui_state.maintain(
&self.client.borrow(),
&mut self.client.borrow_mut(),
&mut self.scene,
debug_info.map(|debug_info| EguiDebugInfo {
frame_time: debug_info.frame_time,

View File

@ -5,12 +5,11 @@ use crate::{
use client::Client;
use egui::FontDefinitions;
use egui_winit_platform::{Platform, PlatformDescriptor};
use voxygen_egui::{DebugShapeAction, EguiDebugInfo, EguiInnerState, EguiWindows};
use voxygen_egui::{EguiAction, EguiDebugInfo, EguiDebugShapeAction, EguiInnerState};
pub struct EguiState {
pub platform: Platform,
egui_inner_state: EguiInnerState,
egui_windows: EguiWindows,
new_debug_shape_id: Option<u64>,
}
@ -27,42 +26,47 @@ impl EguiState {
Self {
platform,
egui_inner_state: EguiInnerState::default(),
egui_windows: EguiWindows::default(),
new_debug_shape_id: None,
}
}
pub fn maintain(
&mut self,
client: &Client,
client: &mut Client,
scene: &mut Scene,
debug_info: Option<EguiDebugInfo>,
) {
let egui_actions = voxygen_egui::maintain(
&mut self.platform,
&mut self.egui_inner_state,
&mut self.egui_windows,
client,
debug_info,
self.new_debug_shape_id.take(),
);
egui_actions.actions.iter().for_each(|action| match action {
DebugShapeAction::AddCylinder { height, radius } => {
let shape_id = scene.debug.add_shape(DebugShape::Cylinder {
height: *height,
radius: *radius,
});
egui_actions
.actions
.into_iter()
.for_each(|action| match action {
EguiAction::ChatCommand { cmd, args } => {
client.send_command(cmd.keyword().into(), args);
},
EguiAction::DebugShape(debug_shape_action) => match debug_shape_action {
EguiDebugShapeAction::AddCylinder { height, radius } => {
let shape_id = scene
.debug
.add_shape(DebugShape::Cylinder { height, radius });
self.new_debug_shape_id = Some(shape_id.0);
},
DebugShapeAction::RemoveShape(debug_shape_id) => {
scene.debug.remove_shape(DebugShapeId(*debug_shape_id));
EguiDebugShapeAction::RemoveShape(debug_shape_id) => {
scene.debug.remove_shape(DebugShapeId(debug_shape_id));
},
DebugShapeAction::SetPosAndColor { id, pos, color } => {
EguiDebugShapeAction::SetPosAndColor { id, pos, color } => {
let identity_ori = [0.0, 0.0, 0.0, 1.0];
scene
.debug
.set_context(DebugShapeId(*id), *pos, *color, identity_ori);
.set_context(DebugShapeId(id), pos, color, identity_ori);
},
},
})
}