editor/
panic.rs

1//! Registers a custom `panic!` handler that alerts the user of unrecoverable errors.
2//!
3//! The source of this module may read a little weird, but this is in fact an intentional workaround
4//! of how the linters function. When running `cargo clippy` it will run *all* targets, including `test`.
5//!
6//! To get around this, when compiling under `test` we generate an empty function (hinted as inline
7//! to further optimise), and for other targets we generate the full method. The `use` statements
8//! are inlined in the method to prevent `unused import` warnings under `test` target.
9
10#[cfg(test)]
11#[inline]
12pub fn register_panic_handler() {}
13
14/// Registers a new `panic!` handler that alerts the user of unrecoverable errors.
15#[cfg(not(test))]
16#[allow(clippy::missing_panics_doc)]
17pub fn register_panic_handler() {
18    use bevy::prelude::error;
19    use rfd::{MessageButtons, MessageDialog};
20    use std::fs::File;
21    use std::io::Write;
22    use std::path::PathBuf;
23    use sysinfo::System;
24
25    let default_hook = std::panic::take_hook();
26    std::panic::set_hook(Box::new(move |info| {
27        let path = std::env::current_dir()
28            .unwrap_or(PathBuf::from("."))
29            .join("crash_report.txt");
30        let message = if let Some(message) = info.payload().downcast_ref::<&'static str>() {
31            String::from(*message)
32        } else {
33            String::from("An unrecoverable error has occurred.")
34        };
35        let location = if let Some(location) = info.location() {
36            location.to_string()
37        } else {
38            String::from("Unknown location")
39        };
40
41        error!("An unrecoverable error has occurred: {:?}", info);
42        MessageDialog::new()
43            .set_level(rfd::MessageLevel::Error)
44            .set_title("Unrecoverable Error")
45            .set_buttons(MessageButtons::Ok)
46            .set_description(format!(
47                "An unrecoverable error has occurred, the editor will shut down.
48The error was: {message}
49
50Error occurred at: {location}
51
52A crash file will be generated at {}",
53                path.display()
54            ))
55            .show();
56
57        let system = System::new_all();
58        let os_version = System::long_os_version().unwrap_or(String::from("Unknown"));
59        let mut dump_file = File::create(path).expect("Failed to create dump file");
60        writeln!(dump_file, "--- DungeonRS Crash Report ---").unwrap();
61        writeln!(
62            dump_file,
63            "Please provide the contents of this file when creating a bug report."
64        )
65        .unwrap();
66        writeln!(dump_file).unwrap();
67        writeln!(dump_file).unwrap();
68        writeln!(dump_file, "Operating System: {os_version}").unwrap();
69        writeln!(
70            dump_file,
71            "Memory: {}/{}",
72            system.used_memory(),
73            system.total_memory()
74        )
75        .unwrap();
76
77        writeln!(
78            dump_file,
79            "CPU: {} Cores {}",
80            system.cpus().len(),
81            system.cpus()[0].brand()
82        )
83        .unwrap();
84        for cpu in system.cpus() {
85            writeln!(
86                dump_file,
87                "{}: {}Hz ({}) {}%",
88                cpu.name(),
89                cpu.frequency(),
90                cpu.brand(),
91                cpu.cpu_usage()
92            )
93            .unwrap();
94        }
95
96        writeln!(dump_file).unwrap();
97        writeln!(dump_file, "---").unwrap();
98        writeln!(dump_file, "Error: {message}").unwrap();
99        writeln!(dump_file, "Location: {location}").unwrap();
100        writeln!(dump_file, "---").unwrap();
101        writeln!(dump_file, "Raw: {info:?}").unwrap();
102
103        dump_file.sync_all().unwrap();
104        default_hook(info);
105    }));
106}