332 lines
10 KiB
Rust
332 lines
10 KiB
Rust
// Don't warn on event sending macros
|
|
#![recursion_limit = "512"]
|
|
#![expect(unused_labels)]
|
|
|
|
#[cfg(target_os = "wasi")]
|
|
compile_error!("Compiling for WASI targets is not supported!");
|
|
|
|
use pumpkin_data::packet::CURRENT_MC_VERSION;
|
|
use std::{
|
|
backtrace::{Backtrace, BacktraceStatus},
|
|
io::{self},
|
|
panic::PanicHookInfo,
|
|
process::exit,
|
|
sync::{Arc, LazyLock, OnceLock, atomic::Ordering},
|
|
thread::{self, ThreadId},
|
|
};
|
|
#[cfg(not(unix))]
|
|
use tokio::signal::ctrl_c;
|
|
#[cfg(unix)]
|
|
use tokio::signal::unix::{SignalKind, signal};
|
|
|
|
use pumpkin::{
|
|
CRASH_REPORT, SERVER_EXIT_CODE, SERVER_IS_STOPPING,
|
|
crash::{CrashReport, FullBacktrace},
|
|
data::VanillaData,
|
|
stop_or_exit_server,
|
|
};
|
|
use pumpkin::{LoggerOption, PumpkinServer, SHOULD_STOP, STOP_INTERRUPT, stop_server};
|
|
|
|
use pumpkin_config::{AdvancedConfiguration, BasicConfiguration, LoadConfiguration};
|
|
use pumpkin_util::text::{
|
|
TextComponent,
|
|
color::{Color, NamedColor},
|
|
};
|
|
use std::time::Instant;
|
|
use tracing::{debug, info, warn};
|
|
|
|
// Setup some tokens to allow us to identify which event is for which socket.
|
|
|
|
pub mod block;
|
|
pub mod command;
|
|
pub mod crash;
|
|
pub mod data;
|
|
pub mod entity;
|
|
pub mod error;
|
|
pub mod item;
|
|
pub mod logging;
|
|
pub mod net;
|
|
pub mod plugin;
|
|
pub mod server;
|
|
pub mod world;
|
|
|
|
pub static LOGGER_IMPL: LazyLock<Arc<OnceLock<LoggerOption>>> =
|
|
LazyLock::new(|| Arc::new(OnceLock::new()));
|
|
|
|
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
static MAIN_THREAD: OnceLock<ThreadId> = OnceLock::new();
|
|
|
|
// WARNING: All rayon calls from the tokio runtime must be non-blocking! This includes things
|
|
// like `par_iter`. These should be spawned in the the rayon pool and then passed to the tokio
|
|
// runtime with a channel! See `Level::fetch_chunks` as an example!
|
|
#[allow(clippy::too_many_lines)]
|
|
#[tokio::main]
|
|
async fn main() {
|
|
MAIN_THREAD
|
|
.set(thread::current().id())
|
|
.expect("Expected to successfully set the main thread ID");
|
|
|
|
// Set the panic handler.
|
|
std::panic::set_hook(Box::new(handle_panic));
|
|
|
|
#[cfg(feature = "console-subscriber")]
|
|
console_subscriber::init();
|
|
let time = Instant::now();
|
|
|
|
let exec_dir = std::env::current_dir().unwrap();
|
|
let config_dir = exec_dir.join("config");
|
|
|
|
let basic_config = BasicConfiguration::load(&config_dir);
|
|
let advanced_config = AdvancedConfiguration::load(&config_dir);
|
|
|
|
let vanilla_data = VanillaData::load();
|
|
|
|
pumpkin::init_logger(&advanced_config);
|
|
|
|
info!(
|
|
"{}",
|
|
TextComponent::text(format!(
|
|
"Starting {} {} Minecraft (Protocol {})",
|
|
TextComponent::text("Pumpkin")
|
|
.color_named(NamedColor::Gold)
|
|
.to_pretty_console(),
|
|
TextComponent::text(CARGO_PKG_VERSION.to_string())
|
|
.color_named(NamedColor::Green)
|
|
.to_pretty_console(),
|
|
TextComponent::text(CURRENT_MC_VERSION.protocol_version().to_string())
|
|
.color_named(NamedColor::DarkBlue)
|
|
.to_pretty_console()
|
|
))
|
|
.to_pretty_console(),
|
|
);
|
|
|
|
debug!(
|
|
"Build info: FAMILY: \"{}\", OS: \"{}\", ARCH: \"{}\", BUILD: \"{}\"",
|
|
std::env::consts::FAMILY,
|
|
std::env::consts::OS,
|
|
std::env::consts::ARCH,
|
|
if cfg!(debug_assertions) {
|
|
"Debug"
|
|
} else {
|
|
"Release"
|
|
}
|
|
);
|
|
print_support_links_and_warning();
|
|
|
|
tokio::spawn(async {
|
|
setup_sighandler()
|
|
.await
|
|
.expect("Unable to setup signal handlers");
|
|
});
|
|
|
|
let pumpkin_server = PumpkinServer::new(basic_config, advanced_config, vanilla_data).await;
|
|
let plugin_wait_time = pumpkin_server.init_plugins().await;
|
|
|
|
let time_elapsed = time.elapsed().saturating_sub(plugin_wait_time);
|
|
|
|
info!(
|
|
"Started server; took {}",
|
|
TextComponent::text(format!("{}ms", time_elapsed.as_millis()))
|
|
.color_named(NamedColor::Gold)
|
|
.to_pretty_console()
|
|
);
|
|
let basic_config = &pumpkin_server.server.basic_config;
|
|
info!(
|
|
"Server is now running. Connect using port: {}{}{}",
|
|
if basic_config.java_edition {
|
|
format!(
|
|
"{} {}",
|
|
TextComponent::text("Java Edition:")
|
|
.color_named(NamedColor::Yellow)
|
|
.to_pretty_console(),
|
|
TextComponent::text(format!("{}", basic_config.java_edition_address))
|
|
.color_named(NamedColor::DarkBlue)
|
|
.to_pretty_console()
|
|
)
|
|
} else {
|
|
TextComponent::text(String::new()).to_pretty_console()
|
|
},
|
|
if basic_config.java_edition && basic_config.bedrock_edition {
|
|
" | " // Separator if both are enabled
|
|
} else {
|
|
""
|
|
},
|
|
if basic_config.bedrock_edition {
|
|
format!(
|
|
"{} {}",
|
|
TextComponent::text("Bedrock Edition:")
|
|
.color_named(NamedColor::Gold)
|
|
.to_pretty_console(),
|
|
TextComponent::text(format!("{}", basic_config.bedrock_edition_address))
|
|
.color_named(NamedColor::DarkBlue)
|
|
.to_pretty_console()
|
|
)
|
|
} else {
|
|
TextComponent::text(String::new()).to_pretty_console()
|
|
}
|
|
);
|
|
|
|
pumpkin_server.start().await;
|
|
|
|
info!(
|
|
"{}",
|
|
TextComponent::text("The server has stopped.")
|
|
.color_named(NamedColor::Red)
|
|
.to_pretty_console()
|
|
);
|
|
|
|
exit(SERVER_EXIT_CODE.load(Ordering::Acquire));
|
|
}
|
|
fn print_support_links_and_warning() {
|
|
warn!(
|
|
"{}",
|
|
TextComponent::text("Pumpkin is currently under heavy development!")
|
|
.color_named(NamedColor::DarkRed)
|
|
.to_pretty_console(),
|
|
);
|
|
info!(
|
|
"Report issues on {}",
|
|
TextComponent::text("https://github.com/Pumpkin-MC/Pumpkin/issues")
|
|
.color_named(NamedColor::DarkAqua)
|
|
.to_pretty_console()
|
|
);
|
|
info!(
|
|
"Join our {} for community support: {}",
|
|
TextComponent::text("Discord")
|
|
.color_named(NamedColor::DarkBlue)
|
|
.to_pretty_console(),
|
|
TextComponent::text("https://discord.gg/pumpkinmc")
|
|
.color_named(NamedColor::Aqua)
|
|
.to_pretty_console()
|
|
);
|
|
}
|
|
|
|
fn handle_interrupt() {
|
|
warn!(
|
|
"{}",
|
|
TextComponent::text("Received interrupt signal; stopping server...")
|
|
.color_named(NamedColor::Red)
|
|
.to_pretty_console()
|
|
);
|
|
stop_or_exit_server();
|
|
}
|
|
|
|
fn handle_panic(panic_info: &PanicHookInfo<'_>) {
|
|
// Generate a crash report.
|
|
let crash_report = {
|
|
// We capture the backtraces here, and not in the
|
|
// crash report, so that the backtrace doesn't show
|
|
// the CrashReport's `new` function.
|
|
let captured_backtrace = Backtrace::capture();
|
|
let full_backtrace = if captured_backtrace.status() == BacktraceStatus::Captured {
|
|
FullBacktrace::Captured
|
|
} else {
|
|
FullBacktrace::ForceCaptured(Backtrace::force_capture())
|
|
};
|
|
|
|
CrashReport::new(panic_info, captured_backtrace, full_backtrace)
|
|
};
|
|
|
|
let payload = panic_info.payload();
|
|
|
|
if is_main_thread() {
|
|
// It's the first panic;
|
|
// We cannot gracefully shut down as the main thread
|
|
// has panicked. However, we can still generate the crash report.
|
|
|
|
if let Some(crash_report) = try_set_crash_report(crash_report) {
|
|
crash_report.print_to_console();
|
|
crash_report.save_and_log();
|
|
|
|
tracing::error!(
|
|
"{}",
|
|
TextComponent::text("Aborting due to the main thread panicking.")
|
|
.color(Color::Named(NamedColor::Red))
|
|
.to_pretty_console()
|
|
);
|
|
} else {
|
|
// It's a subsequent panic.
|
|
tracing::error!(
|
|
"{}: {}",
|
|
TextComponent::text(
|
|
"The main thread panicked while stopping the server; aborting."
|
|
)
|
|
.color(Color::Named(NamedColor::Red))
|
|
.bold()
|
|
.to_pretty_console(),
|
|
payload
|
|
.downcast_ref::<&str>()
|
|
.copied()
|
|
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
|
|
.unwrap_or("<unknown>")
|
|
);
|
|
}
|
|
|
|
exit(1);
|
|
}
|
|
|
|
if try_set_crash_report(crash_report).is_some() {
|
|
// It's the first panic; let's stop the server.
|
|
stop_server();
|
|
} else {
|
|
// It's a subsequent panic; let's just alert about it.
|
|
tracing::error!(
|
|
"{}: {}",
|
|
TextComponent::text("Encountered panic while shutting down")
|
|
.color(Color::Named(NamedColor::Red))
|
|
.bold()
|
|
.to_pretty_console(),
|
|
payload
|
|
.downcast_ref::<&str>()
|
|
.copied()
|
|
.or_else(|| payload.downcast_ref::<String>().map(String::as_str))
|
|
.unwrap_or("<unknown>")
|
|
);
|
|
}
|
|
}
|
|
|
|
fn is_main_thread() -> bool {
|
|
Some(&thread::current().id()) == MAIN_THREAD.get()
|
|
}
|
|
|
|
/// Returns `Some` if the crash report was successfully set. That
|
|
/// means it is the first panic, and it must be logged and saved later.
|
|
///
|
|
/// Returns `None` otherwise as the panic is subsequent.
|
|
fn try_set_crash_report(crash_report: CrashReport) -> Option<&'static CrashReport> {
|
|
if !SERVER_IS_STOPPING.load(Ordering::Acquire) && CRASH_REPORT.set(crash_report).is_ok() {
|
|
CRASH_REPORT.get()
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// Non-UNIX Ctrl-C handling
|
|
#[cfg(not(unix))]
|
|
async fn setup_sighandler() -> io::Result<()> {
|
|
if ctrl_c().await.is_ok() {
|
|
handle_interrupt();
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Unix signal handling
|
|
#[cfg(unix)]
|
|
async fn setup_sighandler() -> io::Result<()> {
|
|
if signal(SignalKind::interrupt())?.recv().await.is_some() {
|
|
handle_interrupt();
|
|
}
|
|
|
|
if signal(SignalKind::hangup())?.recv().await.is_some() {
|
|
handle_interrupt();
|
|
}
|
|
|
|
if signal(SignalKind::terminate())?.recv().await.is_some() {
|
|
handle_interrupt();
|
|
}
|
|
|
|
Ok(())
|
|
}
|