Skip to main content

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::{self, Poise, PoiseState, inventory::item::armor::Friction},
15    resources::Time,
16    uid,
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 uids = client.state().ecs().read_storage::<uid::Uid>();
335        let client_pos = positions.get(client.entity());
336
337        Window::new("ECS Entities")
338            .open(&mut windows.ecs_entities)
339            .default_width(500.0)
340            .default_height(500.0)
341            .show(ctx, |ui| {
342                ui.label(format!("Entity count: {}", ecs.entities().join().count()));
343                ui.add(
344                    Slider::new(&mut max_entity_distance, 1.0..=100000.0)
345                        .logarithmic(true)
346                        .clamping(egui::SliderClamping::Always)
347                        .text("Max entity distance"),
348                );
349
350                ui.add(
351                    Slider::new(&mut selected_entity_cylinder_height, 0.1..=100.0)
352                        .logarithmic(true)
353                        .clamping(egui::SliderClamping::Always)
354                        .text("Cylinder height"),
355                );
356
357                let scroll_area = ScrollArea::vertical().max_height(800.0);
358                scroll_area.show(ui, |ui| {
359                    Grid::new("entities_grid")
360                        .spacing([40.0, 4.0])
361                        .max_col_width(300.0)
362                        .striped(true)
363                        .show(ui, |ui| {
364                            ui.label("-");
365                            ui.label("ID");
366                            ui.label("Pos");
367                            ui.label("Vel");
368                            ui.label("Name");
369                            ui.label("Body");
370                            ui.label("Poise");
371                            ui.label("Character State");
372                            ui.label("Character Activity");
373                            ui.end_row();
374                            for (
375                                entity,
376                                body,
377                                stats,
378                                pos,
379                                _ori,
380                                vel,
381                                poise,
382                                character_state,
383                                character_activity,
384                            ) in (
385                                &ecs.entities(),
386                                ecs.read_storage::<Body>().maybe(),
387                                ecs.read_storage::<comp::Stats>().maybe(),
388                                ecs.read_storage::<comp::Pos>().maybe(),
389                                ecs.read_storage::<comp::Ori>().maybe(),
390                                ecs.read_storage::<comp::Vel>().maybe(),
391                                ecs.read_storage::<Poise>().maybe(),
392                                ecs.read_storage::<comp::CharacterState>().maybe(),
393                                ecs.read_storage::<comp::CharacterActivity>().maybe(),
394                            )
395                                .join()
396                                .filter(
397                                    |(_, _, _, pos, _, _, _, _, _)| {
398                                        client_pos.is_none_or(|client_pos| {
399                                            pos.map_or(0.0, |pos| {
400                                                pos.0.distance_squared(client_pos.0)
401                                            }) < max_entity_distance
402                                        })
403                                    },
404                                )
405                            {
406                                if ui.button("View").clicked() {
407                                    previous_selected_entity =
408                                        mem::take(&mut egui_state.selected_entity_info);
409
410                                    if pos.is_some() {
411                                        egui_actions.actions.push(EguiAction::DebugShape(
412                                            EguiDebugShapeAction::AddCylinder {
413                                                radius: 1.0,
414                                                height: egui_state.selected_entity_cylinder_height,
415                                            },
416                                        ));
417                                    }
418                                    egui_state.selected_entity_info =
419                                        Some(SelectedEntityInfo::new(entity.id()));
420                                }
421
422                                ui.label(format!("{}", entity.id()));
423
424                                if let Some(pos) = pos {
425                                    ui.horizontal(|ui| {
426                                        ui.label(format!(
427                                            "{:.0},{:.0},{:.0}",
428                                            pos.0.x, pos.0.y, pos.0.z
429                                        ));
430
431                                        if let Some(uid) = uids.get(entity)
432                                            && ui.button("GoTo").clicked()
433                                        {
434                                            // Teleports the client to this position
435                                            egui_actions.actions.push(EguiAction::ChatCommand {
436                                                cmd: ServerChatCommand::Tp,
437                                                args: vec![format!("uid@{uid}")],
438                                            })
439                                        }
440                                    });
441                                } else {
442                                    ui.label("-");
443                                }
444
445                                if let Some(vel) = vel {
446                                    ui.label(format!("{:.1}u/s", vel.0.magnitude()));
447                                } else {
448                                    ui.label("-");
449                                }
450                                if let Some(stats) = stats {
451                                    ui.label(format!("{:?}", stats.name));
452                                } else {
453                                    ui.label("-");
454                                }
455                                if let Some(body) = body {
456                                    ui.label(body_species(body));
457                                } else {
458                                    ui.label("-");
459                                }
460
461                                if let Some(poise) = poise {
462                                    poise_state_label(ui, poise);
463                                } else {
464                                    ui.label("-");
465                                }
466
467                                if let Some(character_state) = character_state {
468                                    ui.label(character_state.to_string());
469                                } else {
470                                    ui.label("-");
471                                }
472
473                                if let Some(character_activity) = character_activity {
474                                    ui.label(format!("{:?}", character_activity));
475                                } else {
476                                    ui.label("-");
477                                }
478
479                                ui.end_row();
480                            }
481                        });
482                    let margin = ui.visuals().clip_rect_margin;
483
484                    let current_scroll = ui.clip_rect().top() - ui.min_rect().top() + margin;
485                    let max_scroll =
486                        ui.min_rect().height() - ui.clip_rect().height() + 2.0 * margin;
487                    (current_scroll, max_scroll)
488                });
489            });
490        if let Some(selected_entity_info) = &mut egui_state.selected_entity_info {
491            let selected_entity = ecs.entities().entity(selected_entity_info.entity_id);
492            if !selected_entity.r#gen().is_alive() {
493                previous_selected_entity = mem::take(&mut egui_state.selected_entity_info);
494            } else {
495                selected_entity_window(ctx, ecs, selected_entity_info, &mut egui_actions);
496            }
497        }
498    }
499
500    draw_admin_commands_window(
501        ctx,
502        &mut egui_state.admin_command_state,
503        &mut windows.admin_commands,
504        &mut egui_actions,
505    );
506
507    draw_experimental_shaders_window(
508        ctx,
509        &mut windows.experimental_shaders,
510        &mut egui_actions,
511        &experimental_shaders,
512    );
513
514    if let Some(previous) = previous_selected_entity
515        && let Some(debug_shape_id) = previous.debug_shape_id
516    {
517        egui_actions
518            .actions
519            .push(EguiAction::DebugShape(EguiDebugShapeAction::RemoveShape(
520                debug_shape_id,
521            )));
522    };
523
524    if let Some(selected_entity) = &egui_state.selected_entity_info
525        && let Some(debug_shape_id) = selected_entity.debug_shape_id
526        && (egui_state.selected_entity_cylinder_height - selected_entity_cylinder_height).abs()
527            > f32::EPSILON
528    {
529        egui_actions
530            .actions
531            .push(EguiAction::DebugShape(EguiDebugShapeAction::RemoveShape(
532                debug_shape_id,
533            )));
534        egui_actions
535            .actions
536            .push(EguiAction::DebugShape(EguiDebugShapeAction::AddCylinder {
537                radius: 1.0,
538                height: selected_entity_cylinder_height,
539            }));
540    };
541    if debug_vectors_enabled_mut != egui_state.debug_vectors_enabled {
542        egui_actions
543            .actions
544            .push(EguiAction::SetShowDebugVector(debug_vectors_enabled_mut));
545        egui_state.debug_vectors_enabled = debug_vectors_enabled_mut;
546    }
547
548    egui_state.max_entity_distance = max_entity_distance;
549    egui_state.selected_entity_cylinder_height = selected_entity_cylinder_height;
550    egui_state.windows = windows;
551    egui_actions
552}
553
554fn selected_entity_window(
555    ctx: &Context,
556    ecs: &World,
557    selected_entity_info: &mut SelectedEntityInfo,
558    egui_actions: &mut EguiActions,
559) {
560    let entity_id = selected_entity_info.entity_id;
561    for (
562        _entity,
563        body,
564        stats,
565        pos,
566        _ori,
567        vel,
568        poise,
569        buffs,
570        auras,
571        character_state,
572        character_activity,
573        physics_state,
574        alignment,
575        scale,
576        (mass, density, health, energy),
577    ) in (
578        &ecs.entities(),
579        ecs.read_storage::<Body>().maybe(),
580        ecs.read_storage::<comp::Stats>().maybe(),
581        ecs.read_storage::<comp::Pos>().maybe(),
582        ecs.read_storage::<comp::Ori>().maybe(),
583        ecs.read_storage::<comp::Vel>().maybe(),
584        ecs.read_storage::<Poise>().maybe(),
585        ecs.read_storage::<comp::Buffs>().maybe(),
586        ecs.read_storage::<comp::Auras>().maybe(),
587        ecs.read_storage::<comp::CharacterState>().maybe(),
588        ecs.read_storage::<comp::CharacterActivity>().maybe(),
589        ecs.read_storage::<comp::PhysicsState>().maybe(),
590        ecs.read_storage::<comp::Alignment>().maybe(),
591        ecs.read_storage::<comp::Scale>().maybe(),
592        (
593            ecs.read_storage::<comp::Mass>().maybe(),
594            ecs.read_storage::<comp::Density>().maybe(),
595            ecs.read_storage::<comp::Health>().maybe(),
596            ecs.read_storage::<comp::Energy>().maybe(),
597        ),
598    )
599        .join()
600        .filter(|(e, _, _, _, _, _, _, _, _, _, _, _, _, _, (_, _, _, _))| e.id() == entity_id)
601    {
602        let time = ecs.read_resource::<Time>();
603        if let Some(pos) = pos
604            && let Some(shape_id) = selected_entity_info.debug_shape_id
605        {
606            egui_actions.actions.push(EguiAction::DebugShape(
607                EguiDebugShapeAction::SetPosAndColor {
608                    id: shape_id,
609                    color: [1.0, 1.0, 0.0, 0.5],
610                    pos: [pos.0.x, pos.0.y, pos.0.z + 2.0, 0.0],
611                },
612            ));
613        };
614
615        Window::new("Selected Entity")
616            .default_width(300.0)
617            .default_height(200.0)
618            .show(ctx, |ui| {
619                ui.vertical(|ui| {
620                    CollapsingHeader::new("General").default_open(true).show(ui, |ui| {
621                        Grid::new("selected_entity_general_grid")
622                            .spacing([40.0, 4.0])
623                            .striped(true)
624                            .show(ui, |ui| {
625                                two_col_row(ui, "Health", health.map_or("-".to_owned(), |x| format!("{:.1}/{:.1}", x.current(), x.maximum())));
626                                two_col_row(ui, "Energy", energy.map_or("-".to_owned(), |x| format!("{:.1}/{:.1}", x.current(), x.maximum())));
627                                two_col_row(ui, "Position", pos.map_or("-".to_owned(), |x| format!("({:.1},{:.1},{:.1})", x.0.x, x.0.y, x.0.z)));
628                                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())));
629                                two_col_row(ui, "Alignment", alignment.map_or("-".to_owned(), |x| format!("{:?}", x)));
630                                two_col_row(ui, "Scale", scale.map_or("-".to_owned(), |x| format!("{:?}", x)));
631                                two_col_row(ui, "Mass", mass.map_or("-".to_owned(), |x| format!("{:.1}", x.0)));
632                                two_col_row(ui, "Density", density.map_or("-".to_owned(), |x| format!("{:.1}", x.0)));
633                            });
634
635                    });
636                if let Some(stats) = stats {
637                    CollapsingHeader::new("Stats").default_open(true).show(ui, |ui| {
638                        Grid::new("selected_entity_stats_grid")
639                            .spacing([40.0, 4.0])
640                            .striped(true)
641                            .show(ui, |ui| {
642                                two_col_row(ui, "Name", format!("{:?}", stats.name));
643                                two_col_row(ui, "Damage Reduction", format!("{:.1}", stats.damage_reduction.modifier()));
644                                two_col_row(ui, "Multiplicative Max Health Modifier", format!("{:.1}", stats.max_health_modifiers.mult_mod));
645                                two_col_row(ui, "Move Speed Modifier", format!("{:.1}", stats.move_speed_modifier));
646                            });
647
648                    });
649                }
650                if let Some(body) = body {
651                    CollapsingHeader::new("Body").default_open(false).show(ui, |ui| {
652                        Grid::new("selected_entity_body_grid")
653                            .spacing([40.0, 4.0])
654                            .striped(true)
655                            .show(ui, |ui| {
656                                two_col_row(ui, "Type", body.to_string());
657                                two_col_row(ui, "Species", body_species(body));
658                            });
659
660                    });
661                }
662                if let Some(pos) = pos {
663                    CollapsingHeader::new("Pos").default_open(false).show(ui, |ui| {
664                            Grid::new("selected_entity_pos_grid")
665                                .spacing([40.0, 4.0])
666                                .max_col_width(100.0)
667                                .striped(true)
668                                .show(ui, |ui| {
669                                    two_col_row(ui, "x", format!("{:.1}", pos.0.x));
670                                    two_col_row(ui, "y", format!("{:.1}", pos.0.y));
671                                    two_col_row(ui, "z", format!("{:.1}", pos.0.z));
672                                });
673
674                    });
675                }
676                if let Some(poise) = poise {
677                    CollapsingHeader::new("Poise").default_open(false).show(ui, |ui| {
678                            Grid::new("selected_entity_poise_grid")
679                                .spacing([40.0, 4.0])
680                                .max_col_width(100.0)
681                                .striped(true)
682                                // Apparently, if the #[rustfmt::skip] is in front of the closure scope, rust-analyzer can't
683                                // parse the code properly. Things will *sometimes* work if the skip is on the other side of
684                                // the opening bracket (even though that should only skip formatting the first line of the
685                                // closure), but things as arbitrary as adding a comment to the code cause it to be formatted
686                                // again. Thus, there is a completely pointless inner scope in this closure, just so that the
687                                // code doesn't take up an unreasonable amount of space when formatted. We need that space for
688                                // interesting and educational code comments like this one.
689                                .show(ui, |ui| { #[rustfmt::skip] {
690                                    ui.label("State");
691                                    poise_state_label(ui, poise);
692                                    ui.end_row();
693                                    two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum()));
694                                    two_col_row(ui, "Base Max", format!("{:.1}", poise.base_max()));
695                                }});
696                        });
697                }
698
699                if let Some(buffs) = buffs {
700                    CollapsingHeader::new("Buffs").default_open(false).show(ui, |ui| {
701                        Grid::new("selected_entity_buffs_grid")
702                            .spacing([40.0, 4.0])
703                            .max_col_width(100.0)
704                            .striped(true)
705                            .show(ui, |ui| {
706                                ui.label("Kind");
707                                ui.label("Time");
708                                ui.label("Source");
709                                ui.end_row();
710                                buffs.buffs.iter().for_each(|(_, v)| {
711                                    ui.label(format!("{:?}", v.kind));
712                                    ui.label(
713                                        v.end_time.map_or("-".to_string(), |end| {
714                                            format!("{:?}", end.0 - time.0)
715                                        }),
716                                    );
717                                    ui.label(format!("{:?}", v.source));
718                                    ui.end_row();
719                                });
720                            });
721                    });
722                }
723
724                if let Some(auras) = auras {
725                    CollapsingHeader::new("Auras").default_open(false).show(ui, |ui| {
726                        Grid::new("selected_entity_auras_grid")
727                            .spacing([40.0, 4.0])
728                            .striped(true)
729                            .show(ui, |ui| {
730                                ui.label("Kind");
731                                ui.label("Radius");
732                                ui.label("Duration");
733                                ui.label("Target");
734                                ui.end_row();
735                                auras.auras.iter().for_each(|(_, v)| {
736                                    ui.label(match v.aura_kind {
737                                        Buff { kind, .. } =>  format!("Buff - {:?}", kind),
738                                        FriendlyFire =>  "Friendly Fire".to_string(),
739                                        ForcePvP =>  "ForcedPvP".to_string(),
740                                    });
741                                    ui.label(format!("{:1}", v.radius));
742                                    ui.label(v.end_time.map_or("-".to_owned(), |x| format!("{:1}s", x.0 - time.0)));
743                                    ui.label(format!("{:?}", v.target));
744                                    ui.end_row();
745                                });
746                            });
747                    });
748                }
749
750                if let Some(character_state) = character_state {
751                    if selected_entity_info
752                        .character_state_history
753                        .first()
754                        .unwrap_or(&"-".to_owned())
755                        != &character_state.to_string()
756                    {
757                        selected_entity_info
758                            .character_state_history
759                            .insert(0, character_state.to_string());
760                        if selected_entity_info.character_state_history.len() > 50 {
761                            selected_entity_info.character_state_history.pop();
762                        }
763                    }
764
765                    CollapsingHeader::new("Character State").default_open(false).show(ui, |ui| {
766                        draw_char_state_group(ui, selected_entity_info, character_state);
767                    });
768                }
769
770                if let Some(character_activity) = character_activity {
771                    CollapsingHeader::new("CharacterActivity").default_open(false).show(ui, |ui| {
772                            Grid::new("selected_entity_character_activity_grid")
773                                .spacing([40.0, 4.0])
774                                .max_col_width(100.0)
775                                .striped(true)
776                                .show(ui, |ui| {
777                                    two_col_row(ui, "look_dir", format!("{:.3?}", character_activity.look_dir));
778                                });
779                    });
780                }
781
782                if let Some(physics_state) = physics_state {
783                    CollapsingHeader::new("Physics State").default_open(false).show(ui, |ui| {
784                        Grid::new("selected_entity_physics_state_grid")
785                            .spacing([40.0, 4.0])
786                            .striped(true)
787                            .show(ui, |ui| {
788                                two_col_row(ui, "On Ground", physics_state.on_ground.map_or("None".to_owned(), |x| format!("{:?}", x)));
789                                two_col_row(ui, "On Ceiling", (if physics_state.on_ceiling { "True" } else { "False " }).to_string());
790                                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 )));
791                                two_col_row(ui, "Touching Entities", physics_state.touch_entities.len().to_string());
792                                two_col_row(ui, "In Fluid", match physics_state.in_fluid {
793                                    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()),
794                                    Some(Fluid::Liquid { depth, kind, .. }) => format!("{:?} (Depth: {:.1})", kind, depth),
795                                    _ => "None".to_owned() });
796                                });
797                                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());
798                            });
799                }
800            });
801        });
802    }
803}
804
805fn body_species(body: &Body) -> String {
806    match body {
807        Body::Humanoid(body) => format!("{:?}", body.species),
808        Body::QuadrupedSmall(body) => format!("{:?}", body.species),
809        Body::QuadrupedMedium(body) => format!("{:?}", body.species),
810        Body::BirdMedium(body) => format!("{:?}", body.species),
811        Body::FishMedium(body) => format!("{:?}", body.species),
812        Body::Dragon(body) => format!("{:?}", body.species),
813        Body::BirdLarge(body) => format!("{:?}", body.species),
814        Body::FishSmall(body) => format!("{:?}", body.species),
815        Body::BipedLarge(body) => format!("{:?}", body.species),
816        Body::BipedSmall(body) => format!("{:?}", body.species),
817        Body::Object(body) => format!("{:?}", body),
818        Body::Item(body) => format!("{:?}", body),
819        Body::Golem(body) => format!("{:?}", body.species),
820        Body::Theropod(body) => format!("{:?}", body.species),
821        Body::QuadrupedLow(body) => format!("{:?}", body.species),
822        Body::Arthropod(body) => format!("{:?}", body.species),
823        Body::Ship(body) => format!("{:?}", body),
824        Body::Crustacean(body) => format!("{:?}", body.species),
825        Body::Plugin(body) => format!("{:?}", body),
826    }
827}
828
829fn poise_state_label(ui: &mut Ui, poise: &Poise) {
830    match poise.poise_state() {
831        PoiseState::Normal => {
832            ui.label("Normal");
833        },
834        PoiseState::Interrupted => {
835            ui.colored_label(Color32::YELLOW, "Interrupted");
836        },
837        PoiseState::Stunned => {
838            ui.colored_label(Color32::RED, "Stunned");
839        },
840        PoiseState::Dazed => {
841            ui.colored_label(Color32::RED, "Dazed");
842        },
843        PoiseState::KnockedDown => {
844            ui.colored_label(Color32::BLUE, "Knocked Down");
845        },
846    };
847}