1#![feature(stmt_expr_attributes)]
2#![expect(
3 clippy::needless_pass_by_ref_mut )]
5
6#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
7compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
8
9mod admin;
10mod character_states;
11mod experimental_shaders;
12mod widgets;
13
14use client::{Client, Join, LendJoin, World, WorldExt};
15use common::{
16 cmd::ServerChatCommand,
17 comp,
18 comp::{Poise, PoiseState, inventory::item::armor::Friction},
19 resources::Time,
20};
21use core::mem;
22use egui::{CollapsingHeader, Color32, Grid, Pos2, ScrollArea, Slider, Ui, Window};
23use egui_plot::{Line, Plot, PlotPoints};
24
25use crate::{
26 admin::draw_admin_commands_window, character_states::draw_char_state_group,
27 experimental_shaders::draw_experimental_shaders_window, widgets::two_col_row,
28};
29use common::comp::{
30 Body, Fluid,
31 aura::AuraKind::{Buff, ForcePvP, FriendlyFire},
32};
33use egui_winit_platform::Platform;
34use std::time::Duration;
35#[cfg(feature = "use-dyn-lib")]
36use {
37 common_dynlib::LoadedLib, lazy_static::lazy_static, std::ffi::CStr, std::sync::Arc,
38 std::sync::Mutex,
39};
40
41#[cfg(feature = "use-dyn-lib")]
42lazy_static! {
43 static ref LIB: Arc<Mutex<Option<LoadedLib>>> =
44 common_dynlib::init("veloren-voxygen-egui", "egui", &[]);
45}
46
47#[cfg(feature = "use-dyn-lib")]
48const MAINTAIN_EGUI_FN: &[u8] = b"maintain_egui_inner\0";
49
50pub struct SelectedEntityInfo {
51 entity_id: u32,
52 debug_shape_id: Option<u64>,
53 character_state_history: Vec<String>,
54}
55
56impl SelectedEntityInfo {
57 fn new(entity_id: u32) -> Self {
58 Self {
59 entity_id,
60 debug_shape_id: None,
61 character_state_history: Vec::new(),
62 }
63 }
64}
65
66pub struct AdminCommandState {
67 give_item_qty: u32,
68 give_item_selected_idx: usize,
69 give_item_search_text: String,
70 kits_selected_idx: usize,
71 spawn_entity_qty: u32,
72 spawn_entity_selected_idx: usize,
73 spawn_entity_search_text: String,
74}
75
76impl AdminCommandState {
77 fn new() -> Self {
78 Self {
79 give_item_qty: 1,
80 give_item_selected_idx: 0,
81 give_item_search_text: String::new(),
82 kits_selected_idx: 0,
83 spawn_entity_qty: 1,
84 spawn_entity_selected_idx: 0,
85 spawn_entity_search_text: String::new(),
86 }
87 }
88}
89
90pub struct EguiDebugInfo {
91 pub frame_time: Duration,
92 pub ping_ms: f64,
93}
94
95pub struct EguiInnerState {
96 selected_entity_info: Option<SelectedEntityInfo>,
97 admin_command_state: AdminCommandState,
98 max_entity_distance: f32,
99 selected_entity_cylinder_height: f32,
100 frame_times: Vec<f32>,
101 windows: EguiWindows,
102 debug_vectors_enabled: bool,
103}
104
105#[derive(Clone, Default)]
106pub struct EguiWindows {
107 admin_commands: bool,
108 egui_inspection: bool,
109 egui_settings: bool,
110 egui_memory: bool,
111 frame_time: bool,
112 ecs_entities: bool,
113 experimental_shaders: bool,
114}
115
116impl Default for EguiInnerState {
117 fn default() -> Self {
118 Self {
119 admin_command_state: AdminCommandState::new(),
120 selected_entity_info: None,
121 max_entity_distance: 100000.0,
122 selected_entity_cylinder_height: 10.0,
123 frame_times: Vec::new(),
124 windows: EguiWindows::default(),
125 debug_vectors_enabled: false,
126 }
127 }
128}
129
130pub enum EguiDebugShapeAction {
131 AddCylinder {
132 radius: f32,
133 height: f32,
134 },
135 RemoveShape(u64),
136 SetPosAndColor {
137 id: u64,
138 pos: [f32; 4],
139 color: [f32; 4],
140 },
141}
142
143pub enum EguiAction {
144 ChatCommand {
145 cmd: ServerChatCommand,
146 args: Vec<String>,
147 },
148 DebugShape(EguiDebugShapeAction),
149 SetExperimentalShader(String, bool),
150 SetShowDebugVector(bool),
151}
152
153#[derive(Default)]
154pub struct EguiActions {
155 pub actions: Vec<EguiAction>,
156}
157
158#[cfg(feature = "use-dyn-lib")]
159pub fn init() { lazy_static::initialize(&LIB); }
160
161pub fn maintain(
162 platform: &mut Platform,
163 egui_state: &mut EguiInnerState,
164 client: &Client,
165 debug_info: Option<EguiDebugInfo>,
166 added_cylinder_shape_id: Option<u64>,
167 experimental_shaders: Vec<(String, bool)>,
168) -> EguiActions {
169 #[cfg(not(feature = "use-dyn-lib"))]
170 {
171 maintain_egui_inner(
172 platform,
173 egui_state,
174 client,
175 debug_info,
176 added_cylinder_shape_id,
177 experimental_shaders,
178 )
179 }
180
181 #[cfg(feature = "use-dyn-lib")]
182 {
183 let lock = LIB.lock().unwrap();
184 let lib = &lock.as_ref().unwrap().lib;
185
186 let maintain_fn: common_dynlib::Symbol<
187 fn(
188 &mut Platform,
189 &mut EguiInnerState,
190 &Client,
191 Option<EguiDebugInfo>,
192 Option<u64>,
193 Vec<(String, bool)>,
194 ) -> EguiActions,
195 > = unsafe { lib.get(MAINTAIN_EGUI_FN) }.unwrap_or_else(|e| {
196 panic!(
197 "Trying to use: {} but had error: {:?}",
198 CStr::from_bytes_with_nul(MAINTAIN_EGUI_FN)
199 .map(CStr::to_str)
200 .unwrap()
201 .unwrap(),
202 e
203 )
204 });
205
206 maintain_fn(
207 platform,
208 egui_state,
209 client,
210 debug_info,
211 added_cylinder_shape_id,
212 experimental_shaders,
213 )
214 }
215}
216
217#[cfg_attr(feature = "be-dyn-lib", unsafe(export_name = "maintain_egui_inner"))]
218pub fn maintain_egui_inner(
219 platform: &mut Platform,
220 egui_state: &mut EguiInnerState,
221 client: &Client,
222 debug_info: Option<EguiDebugInfo>,
223 added_cylinder_shape_id: Option<u64>,
224 experimental_shaders: Vec<(String, bool)>,
225) -> EguiActions {
226 platform.begin_pass();
227 let ctx = &platform.context();
228
229 let mut egui_actions = EguiActions::default();
230 let mut previous_selected_entity: Option<SelectedEntityInfo> = None;
231 let mut max_entity_distance = egui_state.max_entity_distance;
232 let mut selected_entity_cylinder_height = egui_state.selected_entity_cylinder_height;
233 let mut windows = egui_state.windows.clone();
234 let mut debug_vectors_enabled_mut = egui_state.debug_vectors_enabled;
235
236 if let Some(shape_id) = added_cylinder_shape_id {
239 if let Some(selected_entity) = &mut egui_state.selected_entity_info {
240 selected_entity.debug_shape_id = Some(shape_id);
241 }
242 }
243
244 if let Some(debug_info) = debug_info.as_ref() {
245 egui_state
246 .frame_times
247 .push(debug_info.frame_time.as_nanos() as f32);
248 if egui_state.frame_times.len() > 250 {
249 egui_state.frame_times.remove(0);
250 }
251 };
252
253 let start_pos = Pos2 { x: 300.0, y: 0.0 };
254 Window::new("Debug Control")
255 .default_pos(start_pos)
256 .default_width(200.0)
257 .default_height(200.0)
258 .show(&platform.context(), |ui| {
259 ui.horizontal(|ui| {
260 ui.label(format!(
261 "Ping: {:.1}ms",
262 debug_info.as_ref().map_or(0.0, |x| x.ping_ms)
263 ));
264 });
265 ui.group(|ui| {
266 ui.vertical(|ui| {
267 ui.checkbox(&mut windows.admin_commands, "Admin Commands");
268 ui.checkbox(&mut windows.ecs_entities, "ECS Entities");
269 ui.checkbox(&mut windows.frame_time, "Frame Time");
270 ui.checkbox(&mut windows.experimental_shaders, "Experimental Shaders");
271 ui.checkbox(&mut debug_vectors_enabled_mut, "Show Debug Vectors");
272 });
273 });
274
275 ui.group(|ui| {
276 ui.vertical(|ui| {
277 ui.label("Show EGUI Windows");
278 ui.horizontal(|ui| {
279 ui.checkbox(&mut windows.egui_inspection, "🔍 Inspection");
280 ui.checkbox(&mut windows.egui_settings, "🔧 Settings");
281 ui.checkbox(&mut windows.egui_memory, "📝 Memory");
282 })
283 })
284 });
285 });
286
287 Window::new("🔧 Settings")
288 .open(&mut windows.egui_settings)
289 .vscroll(true)
290 .show(ctx, |ui| {
291 ctx.settings_ui(ui);
292 });
293 Window::new("🔍 Inspection")
294 .open(&mut windows.egui_inspection)
295 .vscroll(true)
296 .show(ctx, |ui| {
297 ctx.inspection_ui(ui);
298 });
299
300 Window::new("📝 Memory")
301 .open(&mut windows.egui_memory)
302 .resizable(false)
303 .show(ctx, |ui| {
304 ctx.memory_ui(ui);
305 });
306
307 Window::new("Frame Time")
308 .open(&mut windows.frame_time)
309 .default_width(200.0)
310 .default_height(200.0)
311 .show(ctx, |ui| {
312 Plot::new("Frame Time").show(ui, |plot_ui| {
313 plot_ui.line(Line::new(
314 "Frame Time",
315 PlotPoints::from_iter(
316 egui_state
317 .frame_times
318 .iter()
319 .enumerate()
320 .map(|(i, x)| [i as f64, *x as f64]),
321 ),
322 ))
323 });
324 });
325
326 if windows.ecs_entities {
327 let ecs = client.state().ecs();
328
329 let positions = client.state().ecs().read_storage::<comp::Pos>();
330 let client_pos = positions.get(client.entity());
331
332 Window::new("ECS Entities")
333 .open(&mut windows.ecs_entities)
334 .default_width(500.0)
335 .default_height(500.0)
336 .show(ctx, |ui| {
337 ui.label(format!("Entity count: {}", &ecs.entities().join().count()));
338 ui.add(
339 Slider::new(&mut max_entity_distance, 1.0..=100000.0)
340 .logarithmic(true)
341 .clamping(egui::SliderClamping::Always)
342 .text("Max entity distance"),
343 );
344
345 ui.add(
346 Slider::new(&mut selected_entity_cylinder_height, 0.1..=100.0)
347 .logarithmic(true)
348 .clamping(egui::SliderClamping::Always)
349 .text("Cylinder height"),
350 );
351
352 let scroll_area = ScrollArea::vertical().max_height(800.0);
353 scroll_area.show(ui, |ui| {
354 Grid::new("entities_grid")
355 .spacing([40.0, 4.0])
356 .max_col_width(300.0)
357 .striped(true)
358 .show(ui, |ui| {
359 ui.label("-");
360 ui.label("ID");
361 ui.label("Pos");
362 ui.label("Vel");
363 ui.label("Name");
364 ui.label("Body");
365 ui.label("Poise");
366 ui.label("Character State");
367 ui.label("Character Activity");
368 ui.end_row();
369 for (
370 entity,
371 body,
372 stats,
373 pos,
374 _ori,
375 vel,
376 poise,
377 character_state,
378 character_activity,
379 ) in (
380 &ecs.entities(),
381 ecs.read_storage::<Body>().maybe(),
382 ecs.read_storage::<comp::Stats>().maybe(),
383 ecs.read_storage::<comp::Pos>().maybe(),
384 ecs.read_storage::<comp::Ori>().maybe(),
385 ecs.read_storage::<comp::Vel>().maybe(),
386 ecs.read_storage::<Poise>().maybe(),
387 ecs.read_storage::<comp::CharacterState>().maybe(),
388 ecs.read_storage::<comp::CharacterActivity>().maybe(),
389 )
390 .join()
391 .filter(
392 |(_, _, _, pos, _, _, _, _, _)| {
393 client_pos.is_none_or(|client_pos| {
394 pos.map_or(0.0, |pos| {
395 pos.0.distance_squared(client_pos.0)
396 }) < max_entity_distance
397 })
398 },
399 )
400 {
401 if ui.button("View").clicked() {
402 previous_selected_entity =
403 mem::take(&mut egui_state.selected_entity_info);
404
405 if pos.is_some() {
406 egui_actions.actions.push(EguiAction::DebugShape(
407 EguiDebugShapeAction::AddCylinder {
408 radius: 1.0,
409 height: egui_state.selected_entity_cylinder_height,
410 },
411 ));
412 }
413 egui_state.selected_entity_info =
414 Some(SelectedEntityInfo::new(entity.id()));
415 }
416
417 ui.label(format!("{}", entity.id()));
418
419 if let Some(pos) = pos {
420 ui.label(format!(
421 "{:.0},{:.0},{:.0}",
422 pos.0.x, pos.0.y, pos.0.z
423 ));
424 } else {
425 ui.label("-");
426 }
427
428 if let Some(vel) = vel {
429 ui.label(format!("{:.1}u/s", vel.0.magnitude()));
430 } else {
431 ui.label("-");
432 }
433 if let Some(stats) = stats {
434 ui.label(format!("{:?}", &stats.name));
435 } else {
436 ui.label("-");
437 }
438 if let Some(body) = body {
439 ui.label(body_species(body));
440 } else {
441 ui.label("-");
442 }
443
444 if let Some(poise) = poise {
445 poise_state_label(ui, poise);
446 } else {
447 ui.label("-");
448 }
449
450 if let Some(character_state) = character_state {
451 ui.label(character_state.to_string());
452 } else {
453 ui.label("-");
454 }
455
456 if let Some(character_activity) = character_activity {
457 ui.label(format!("{:?}", character_activity));
458 } else {
459 ui.label("-");
460 }
461
462 ui.end_row();
463 }
464 });
465 let margin = ui.visuals().clip_rect_margin;
466
467 let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin;
468 let max_scroll =
469 ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin;
470 (current_scroll, max_scroll)
471 });
472 });
473 if let Some(selected_entity_info) = &mut egui_state.selected_entity_info {
474 let selected_entity = ecs.entities().entity(selected_entity_info.entity_id);
475 if !selected_entity.gen().is_alive() {
476 previous_selected_entity = mem::take(&mut egui_state.selected_entity_info);
477 } else {
478 selected_entity_window(platform, ecs, selected_entity_info, &mut egui_actions);
479 }
480 }
481 }
482
483 draw_admin_commands_window(
484 ctx,
485 &mut egui_state.admin_command_state,
486 &mut windows.admin_commands,
487 &mut egui_actions,
488 );
489
490 draw_experimental_shaders_window(
491 ctx,
492 &mut windows.experimental_shaders,
493 &mut egui_actions,
494 &experimental_shaders,
495 );
496
497 if let Some(previous) = previous_selected_entity {
498 if let Some(debug_shape_id) = previous.debug_shape_id {
499 egui_actions
500 .actions
501 .push(EguiAction::DebugShape(EguiDebugShapeAction::RemoveShape(
502 debug_shape_id,
503 )));
504 }
505 };
506
507 if let Some(selected_entity) = &egui_state.selected_entity_info {
508 if let Some(debug_shape_id) = selected_entity.debug_shape_id {
509 if (egui_state.selected_entity_cylinder_height - selected_entity_cylinder_height).abs()
510 > f32::EPSILON
511 {
512 egui_actions.actions.push(EguiAction::DebugShape(
513 EguiDebugShapeAction::RemoveShape(debug_shape_id),
514 ));
515 egui_actions.actions.push(EguiAction::DebugShape(
516 EguiDebugShapeAction::AddCylinder {
517 radius: 1.0,
518 height: selected_entity_cylinder_height,
519 },
520 ));
521 }
522 }
523 };
524 if debug_vectors_enabled_mut != egui_state.debug_vectors_enabled {
525 egui_actions
526 .actions
527 .push(EguiAction::SetShowDebugVector(debug_vectors_enabled_mut));
528 egui_state.debug_vectors_enabled = debug_vectors_enabled_mut;
529 }
530
531 egui_state.max_entity_distance = max_entity_distance;
532 egui_state.selected_entity_cylinder_height = selected_entity_cylinder_height;
533 egui_state.windows = windows;
534 egui_actions
535}
536
537fn selected_entity_window(
538 platform: &mut Platform,
539 ecs: &World,
540 selected_entity_info: &mut SelectedEntityInfo,
541 egui_actions: &mut EguiActions,
542) {
543 let entity_id = selected_entity_info.entity_id;
544 for (
545 _entity,
546 body,
547 stats,
548 pos,
549 _ori,
550 vel,
551 poise,
552 buffs,
553 auras,
554 character_state,
555 character_activity,
556 physics_state,
557 alignment,
558 scale,
559 (mass, density, health, energy),
560 ) in (
561 &ecs.entities(),
562 ecs.read_storage::<Body>().maybe(),
563 ecs.read_storage::<comp::Stats>().maybe(),
564 ecs.read_storage::<comp::Pos>().maybe(),
565 ecs.read_storage::<comp::Ori>().maybe(),
566 ecs.read_storage::<comp::Vel>().maybe(),
567 ecs.read_storage::<Poise>().maybe(),
568 ecs.read_storage::<comp::Buffs>().maybe(),
569 ecs.read_storage::<comp::Auras>().maybe(),
570 ecs.read_storage::<comp::CharacterState>().maybe(),
571 ecs.read_storage::<comp::CharacterActivity>().maybe(),
572 ecs.read_storage::<comp::PhysicsState>().maybe(),
573 ecs.read_storage::<comp::Alignment>().maybe(),
574 ecs.read_storage::<comp::Scale>().maybe(),
575 (
576 ecs.read_storage::<comp::Mass>().maybe(),
577 ecs.read_storage::<comp::Density>().maybe(),
578 ecs.read_storage::<comp::Health>().maybe(),
579 ecs.read_storage::<comp::Energy>().maybe(),
580 ),
581 )
582 .join()
583 .filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _, _))| e.id() == entity_id)
584 {
585 let time = ecs.read_resource::<Time>();
586 if let Some(pos) = pos {
587 if let Some(shape_id) = selected_entity_info.debug_shape_id {
588 egui_actions.actions.push(EguiAction::DebugShape(
589 EguiDebugShapeAction::SetPosAndColor {
590 id: shape_id,
591 color: [1.0, 1.0, 0.0, 0.5],
592 pos: [pos.0.x, pos.0.y, pos.0.z + 2.0, 0.0],
593 },
594 ));
595 }
596 };
597
598 Window::new("Selected Entity")
599 .default_width(300.0)
600 .default_height(200.0)
601 .show(&platform.context(), |ui| {
602 ui.vertical(|ui| {
603 CollapsingHeader::new("General").default_open(true).show(ui, |ui| {
604 Grid::new("selected_entity_general_grid")
605 .spacing([40.0, 4.0])
606 .striped(true)
607 .show(ui, |ui| {
608 two_col_row(ui, "Health", health.map_or("-".to_owned(), |x| format!("{:.1}/{:.1}", x.current(), x.maximum())));
609 two_col_row(ui, "Energy", energy.map_or("-".to_owned(), |x| format!("{:.1}/{:.1}", x.current(), x.maximum())));
610 two_col_row(ui, "Position", pos.map_or("-".to_owned(), |x| format!("({:.1},{:.1},{:.1})", x.0.x, x.0.y, x.0.z)));
611 two_col_row(ui, "Velocity", vel.map_or("-".to_owned(), |x| format!("({:.1},{:.1},{:.1}) ({:.1} u/s)", x.0.x, x.0.y, x.0.z, x.0.magnitude())));
612 two_col_row(ui, "Alignment", alignment.map_or("-".to_owned(), |x| format!("{:?}", x)));
613 two_col_row(ui, "Scale", scale.map_or("-".to_owned(), |x| format!("{:?}", x)));
614 two_col_row(ui, "Mass", mass.map_or("-".to_owned(), |x| format!("{:.1}", x.0)));
615 two_col_row(ui, "Density", density.map_or("-".to_owned(), |x| format!("{:.1}", x.0)));
616 });
617
618 });
619 if let Some(stats) = stats {
620 CollapsingHeader::new("Stats").default_open(true).show(ui, |ui| {
621 Grid::new("selected_entity_stats_grid")
622 .spacing([40.0, 4.0])
623 .striped(true)
624 .show(ui, |ui| {
625 two_col_row(ui, "Name", format!("{:?}", stats.name));
626 two_col_row(ui, "Damage Reduction", format!("{:.1}", stats.damage_reduction.modifier()));
627 two_col_row(ui, "Multiplicative Max Health Modifier", format!("{:.1}", stats.max_health_modifiers.mult_mod));
628 two_col_row(ui, "Move Speed Modifier", format!("{:.1}", stats.move_speed_modifier));
629 });
630
631 });
632 }
633 if let Some(body) = body {
634 CollapsingHeader::new("Body").default_open(false).show(ui, |ui| {
635 Grid::new("selected_entity_body_grid")
636 .spacing([40.0, 4.0])
637 .striped(true)
638 .show(ui, |ui| {
639 two_col_row(ui, "Type", body.to_string());
640 two_col_row(ui, "Species", body_species(body));
641 });
642
643 });
644 }
645 if let Some(pos) = pos {
646 CollapsingHeader::new("Pos").default_open(false).show(ui, |ui| {
647 Grid::new("selected_entity_pos_grid")
648 .spacing([40.0, 4.0])
649 .max_col_width(100.0)
650 .striped(true)
651 .show(ui, |ui| {
652 two_col_row(ui, "x", format!("{:.1}", pos.0.x));
653 two_col_row(ui, "y", format!("{:.1}", pos.0.y));
654 two_col_row(ui, "z", format!("{:.1}", pos.0.z));
655 });
656
657 });
658 }
659 if let Some(poise) = poise {
660 CollapsingHeader::new("Poise").default_open(false).show(ui, |ui| {
661 Grid::new("selected_entity_poise_grid")
662 .spacing([40.0, 4.0])
663 .max_col_width(100.0)
664 .striped(true)
665 .show(ui, |ui| { #[rustfmt::skip] {
673 ui.label("State");
674 poise_state_label(ui, poise);
675 ui.end_row();
676 two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum()));
677 two_col_row(ui, "Base Max", format!("{:.1}", poise.base_max()));
678 }});
679 });
680 }
681
682 if let Some(buffs) = buffs {
683 CollapsingHeader::new("Buffs").default_open(false).show(ui, |ui| {
684 Grid::new("selected_entity_buffs_grid")
685 .spacing([40.0, 4.0])
686 .max_col_width(100.0)
687 .striped(true)
688 .show(ui, |ui| {
689 ui.label("Kind");
690 ui.label("Time");
691 ui.label("Source");
692 ui.end_row();
693 buffs.buffs.iter().for_each(|(_, v)| {
694 ui.label(format!("{:?}", v.kind));
695 ui.label(
696 v.end_time.map_or("-".to_string(), |end| {
697 format!("{:?}", end.0 - time.0)
698 }),
699 );
700 ui.label(format!("{:?}", v.source));
701 ui.end_row();
702 });
703 });
704 });
705 }
706
707 if let Some(auras) = auras {
708 CollapsingHeader::new("Auras").default_open(false).show(ui, |ui| {
709 Grid::new("selected_entity_auras_grid")
710 .spacing([40.0, 4.0])
711 .striped(true)
712 .show(ui, |ui| {
713 ui.label("Kind");
714 ui.label("Radius");
715 ui.label("Duration");
716 ui.label("Target");
717 ui.end_row();
718 auras.auras.iter().for_each(|(_, v)| {
719 ui.label(match v.aura_kind {
720 Buff { kind, .. } => format!("Buff - {:?}", kind),
721 FriendlyFire => "Friendly Fire".to_string(),
722 ForcePvP => "ForcedPvP".to_string(),
723 });
724 ui.label(format!("{:1}", v.radius));
725 ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0)));
726 ui.label(format!("{:?}", v.target));
727 ui.end_row();
728 });
729 });
730 });
731 }
732
733 if let Some(character_state) = character_state {
734 if selected_entity_info
735 .character_state_history
736 .first()
737 .unwrap_or(&"-".to_owned())
738 != &character_state.to_string()
739 {
740 selected_entity_info
741 .character_state_history
742 .insert(0, character_state.to_string());
743 if selected_entity_info.character_state_history.len() > 50 {
744 selected_entity_info.character_state_history.pop();
745 }
746 }
747
748 CollapsingHeader::new("Character State").default_open(false).show(ui, |ui| {
749 draw_char_state_group(ui, selected_entity_info, character_state);
750 });
751 }
752
753 if let Some(character_activity) = character_activity {
754 CollapsingHeader::new("CharacterActivity").default_open(false).show(ui, |ui| {
755 Grid::new("selected_entity_character_activity_grid")
756 .spacing([40.0, 4.0])
757 .max_col_width(100.0)
758 .striped(true)
759 .show(ui, |ui| {
760 two_col_row(ui, "look_dir", format!("{:.3?}", character_activity.look_dir));
761 });
762 });
763 }
764
765 if let Some(physics_state) = physics_state {
766 CollapsingHeader::new("Physics State").default_open(false).show(ui, |ui| {
767 Grid::new("selected_entity_physics_state_grid")
768 .spacing([40.0, 4.0])
769 .striped(true)
770 .show(ui, |ui| {
771 two_col_row(ui, "On Ground", physics_state.on_ground.map_or("None".to_owned(), |x| format!("{:?}", x)));
772 two_col_row(ui, "On Ceiling", (if physics_state.on_ceiling { "True" } else { "False " }).to_string());
773 two_col_row(ui, "On Wall", physics_state.on_wall.map_or("-".to_owned(), |x| format!("{:.1},{:.1},{:.1}", x.x, x.y, x.z )));
774 two_col_row(ui, "Touching Entities", physics_state.touch_entities.len().to_string());
775 two_col_row(ui, "In Fluid", match physics_state.in_fluid {
776 Some(Fluid::Air { elevation, vel, .. }) => format!("Air (Elevation: {:.1}), vel: ({:.1},{:.1},{:.1}) ({:.1} u/s)", elevation, vel.0.x, vel.0.y, vel.0.z, vel.0.magnitude()),
777 Some(Fluid::Liquid { depth, kind, .. }) => format!("{:?} (Depth: {:.1})", kind, depth),
778 _ => "None".to_owned() });
779 });
780 two_col_row(ui, "Footwear", match physics_state.footwear{ Friction::Ski => "Ski", Friction::Skate => "Skate", Friction::Normal=>"Normal",}.to_string());
781 });
782 }
783 });
784 });
785 }
786}
787
788fn body_species(body: &Body) -> String {
789 match body {
790 Body::Humanoid(body) => format!("{:?}", body.species),
791 Body::QuadrupedSmall(body) => format!("{:?}", body.species),
792 Body::QuadrupedMedium(body) => format!("{:?}", body.species),
793 Body::BirdMedium(body) => format!("{:?}", body.species),
794 Body::FishMedium(body) => format!("{:?}", body.species),
795 Body::Dragon(body) => format!("{:?}", body.species),
796 Body::BirdLarge(body) => format!("{:?}", body.species),
797 Body::FishSmall(body) => format!("{:?}", body.species),
798 Body::BipedLarge(body) => format!("{:?}", body.species),
799 Body::BipedSmall(body) => format!("{:?}", body.species),
800 Body::Object(body) => format!("{:?}", body),
801 Body::Item(body) => format!("{:?}", body),
802 Body::Golem(body) => format!("{:?}", body.species),
803 Body::Theropod(body) => format!("{:?}", body.species),
804 Body::QuadrupedLow(body) => format!("{:?}", body.species),
805 Body::Arthropod(body) => format!("{:?}", body.species),
806 Body::Ship(body) => format!("{:?}", body),
807 Body::Crustacean(body) => format!("{:?}", body.species),
808 Body::Plugin(body) => format!("{:?}", body),
809 }
810}
811
812fn poise_state_label(ui: &mut Ui, poise: &Poise) {
813 match poise.poise_state() {
814 PoiseState::Normal => {
815 ui.label("Normal");
816 },
817 PoiseState::Interrupted => {
818 ui.colored_label(Color32::YELLOW, "Interrupted");
819 },
820 PoiseState::Stunned => {
821 ui.colored_label(Color32::RED, "Stunned");
822 },
823 PoiseState::Dazed => {
824 ui.colored_label(Color32::RED, "Dazed");
825 },
826 PoiseState::KnockedDown => {
827 ui.colored_label(Color32::BLUE, "Knocked Down");
828 },
829 };
830}