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