veloren_voxygen_egui/
lib.rs

1#![feature(stmt_expr_attributes)]
2#![expect(
3    clippy::needless_pass_by_ref_mut //until we find a better way for specs
4)]
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 a debug cylinder was added in the last frame, store it against the
237    // selected entity
238    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                                // Apparently, if the #[rustfmt::skip] is in front of the closure scope, rust-analyzer can't
666                                // parse the code properly. Things will *sometimes* work if the skip is on the other side of
667                                // the opening bracket (even though that should only skip formatting the first line of the
668                                // closure), but things as arbitrary as adding a comment to the code cause it to be formatted
669                                // again. Thus, there is a completely pointless inner scope in this closure, just so that the
670                                // code doesn't take up an unreasonable amount of space when formatted. We need that space for
671                                // interesting and educational code comments like this one.
672                                .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::Snowshoe => "Snowshoe", Friction::Spikes => "Spikes", */ 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}