veloren_voxygen_egui/
lib.rs

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