diff --git a/.etc/example-config.toml b/.etc/example-config.toml index cc43e3fc..a0cef8e2 100644 --- a/.etc/example-config.toml +++ b/.etc/example-config.toml @@ -36,3 +36,5 @@ map_size = 1_000 cache_ttl = 60 # How big the cache can be in kb. cache_capacity = 20_000 + +whitelist = false \ No newline at end of file diff --git a/README.md b/README.md index 441bce29..9a4ce882 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p

📝 Custom made network, NBT and Anvil encoding systems to allow for minimal I/O lag

  • -

    💾 Multiple database options to finetune the server to your needs

    +

    💾 Crazy fast K/V database

    32 render distance* Chunk Loading DEMO
  • @@ -94,7 +94,7 @@ our [Discord server](https://discord.gg/qT5J8EMjwk) for help or to discuss the p

    Optimizations

  • -

    Plugin support (JVM currently, other languages will be considered later)

    +

    Plugin support (FFI currently, other languages will be considered later)

  • @@ -148,9 +148,23 @@ cargo build --release ## 🖥️ Usage +```plaintext +Usage: ferrumc.exe [OPTIONS] [COMMAND] + +Commands: +setup Sets up the config +import Import the world data +run Start the server (default, if no command is given) +help Print this message or the help of the given subcommand(s) + +Options: +--log [default: debug] [possible values: trace, debug, info, warn, error] +-h, --help Print help +``` + 1. Move the FerrumC binary (`ferrumc.exe` or `ferrumc` depending on the OS) to your desired server directory 2. Open a terminal in that directory -3. (Optional) Generate a config file: `./ferrumc --setup` +3. (Optional) Generate a config file: `./ferrumc setup` - Edit the generated `config.toml` file to customize your server settings 4. Import an existing world: Either copy your world files to the server directory or specify the path to the world files in the `config.toml` file. This should be the root directory of your world files, containing the `region` directory @@ -218,10 +232,9 @@ with the vanilla server, but we do plan on implementing some sort of terrain gen ### Will there be plugins? And how? -We do very much plan to have a plugin system and as of right now, our plan is to leverage the -JVM to allow for plugins to be written in Kotlin, Java, or any other JVM language. We are also considering other -languages -such as Rust, JavaScript and possibly other native languages, but that is a fair way off for now. +We do very much plan to have a plugin system and as of right now we are planning to use +some kind of ffi (foreign function interface) to allow for plugins to be written in other languages. +Not confirmed yet. ### What does 'FerrumC' mean? diff --git a/scripts/new_packet.py b/scripts/new_packet.py index 9e8d518b..0f493608 100644 --- a/scripts/new_packet.py +++ b/scripts/new_packet.py @@ -61,4 +61,4 @@ def to_camel_case(string) -> str: else: f.write(outgoing_template.replace("++name++", to_camel_case(packet_name)).replace("++id++", packet_id)) with open(f"{packets_dir}/outgoing/mod.rs", "a") as modfile: - modfile.write(f"\npub mod {to_snake_case(packet_name)};") \ No newline at end of file + modfile.write(f"\npub mod {to_snake_case(packet_name)};") diff --git a/src/bin/src/systems/chunk_fetcher.rs b/src/bin/src/systems/chunk_fetcher.rs index 47741926..601a34c4 100644 --- a/src/bin/src/systems/chunk_fetcher.rs +++ b/src/bin/src/systems/chunk_fetcher.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use std::sync::atomic::AtomicBool; use std::sync::Arc; use tokio::task::JoinSet; -use tracing::{error, info, trace}; +use tracing::{debug, info, trace}; pub struct ChunkFetcher { stop: AtomicBool, @@ -70,11 +70,11 @@ impl System for ChunkFetcher { match result { Ok(task_res) => { if let Err(e) = task_res { - error!("Error fetching chunk: {:?}", e); + debug!("Error fetching chunk: {:?}", e); } } Err(e) => { - error!("Error fetching chunk: {:?}", e); + debug!("Error fetching chunk: {:?}", e); } } } diff --git a/src/bin/src/systems/ticking_system.rs b/src/bin/src/systems/ticking_system.rs index ffe6f21f..6e72394f 100644 --- a/src/bin/src/systems/ticking_system.rs +++ b/src/bin/src/systems/ticking_system.rs @@ -7,7 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; -use tracing::{debug, info}; +use tracing::{debug, info, trace}; pub struct TickingSystem; static KILLED: AtomicBool = AtomicBool::new(false); @@ -19,12 +19,18 @@ impl System for TickingSystem { let mut tick = 0; while !KILLED.load(Ordering::Relaxed) { let required_end = Instant::now() + Duration::from_millis(50); - // TODO handle error - let res = TickEvent::trigger(TickEvent::new(tick), state.clone()).await; + let res = { + let start = Instant::now(); + let res = TickEvent::trigger(TickEvent::new(tick), state.clone()).await; + trace!("Tick took {:?}", Instant::now() - start); + + res + }; if res.is_err() { debug!("error handling tick event: {:?}", res); } let now = Instant::now(); + if required_end > now { tokio::time::sleep(required_end - now).await; } else { diff --git a/src/lib/derive_macros/src/net/decode.rs b/src/lib/derive_macros/src/net/decode.rs index 5a964eb9..b3407f27 100644 --- a/src/lib/derive_macros/src/net/decode.rs +++ b/src/lib/derive_macros/src/net/decode.rs @@ -1,4 +1,4 @@ -use crate::helpers::{get_derive_attributes, StructInfo}; +use crate::helpers::{extract_struct_info, get_derive_attributes, StructInfo}; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput, LitStr}; @@ -6,125 +6,124 @@ use syn::{parse_macro_input, DeriveInput, LitStr}; pub(crate) fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); + // Collect attributes relevant to our `net(...)` usage: let net_attributes = get_derive_attributes(&input, "net"); let repr_attr = get_derive_attributes(&input, "repr"); - // check the type of repr attribute - let repr_attr = { - let mut repr_type = None; - repr_attr.iter().for_each(|attr| { - attr.parse_nested_meta(|meta| { - let Some(ident) = meta.path.get_ident() else { - return Ok(()); - }; - - repr_type = Some(ident.to_string()); + // Attempt to parse the `#[repr(...)]` attribute if it exists. + let repr_type = { + let mut repr_t = None; + for attr in &repr_attr { + attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + repr_t = Some(ident.to_string()); + } Ok(()) }) .unwrap(); - }); - - repr_type.map(|val| syn::parse_str::(&val).expect("Failed to parse repr type")) + } + repr_t.map(|val| syn::parse_str::(&val).expect("Failed to parse repr type")) }; - // check if any attribute that has "#[net(u8_cast)]" + // Look for `#[net(type_cast = "X", type_cast_handler = "Y")]` usage for enum casting. let (type_cast, type_cast_handler) = { - let mut type_cast = None; - let mut type_cast_handler = None; - net_attributes.iter().for_each(|attr| { + let mut cast = None; + let mut cast_handler = None; + for attr in &net_attributes { attr.parse_nested_meta(|meta| { - let Some(ident) = meta.path.get_ident() else { - return Ok(()); - }; - - match ident.to_string().as_str() { - "type_cast" => { - let value = meta.value().expect("value failed"); - let value = value.parse::().expect("parse failed"); - let n = value.value(); - type_cast = Some(n); - } - "type_cast_handler" => { - let value = meta.value().expect("value failed"); - let value = value.parse::().expect("parse failed"); - let n = value.value(); - type_cast_handler = Some(n); - } - &_ => { - return Ok(()); + if let Some(ident) = meta.path.get_ident() { + match ident.to_string().as_str() { + "type_cast" => { + let value = meta.value().expect("Missing type_cast value"); + let value = value.parse::().expect("Failed to parse type_cast"); + cast = Some(value.value()); + } + "type_cast_handler" => { + let value = meta.value().expect("Missing type_cast_handler value"); + let value = value + .parse::() + .expect("Failed to parse type_cast_handler"); + cast_handler = Some(value.value()); + } + _ => {} } } - Ok(()) }) .unwrap(); - }); - - (type_cast, type_cast_handler) + } + (cast, cast_handler) }; - // So for enums we can simply read the type and then cast it directly. + // If `type_cast` is present, we assume this is an enum. We'll decode by reading + // the specified type, then casting into the enum. if let Some(type_cast) = type_cast { - let Some(repr_attr) = repr_attr else { - panic!( - "NetDecode with type_cast enabled requires a repr attribute. Example: #[repr(u8)]" - ); + let Some(repr_ident) = repr_type else { + panic!("NetDecode with type_cast requires a repr attribute. Example: #[repr(u8)]"); }; - // in netdecode, read a type of type_cast and then if type_cast_handler exists, use it to do `type_cast_handler(type_cast)` - - let type_cast = syn::parse_str::(&type_cast).expect("Failed to parse type_cast"); + let type_cast_ty = + syn::parse_str::(&type_cast).expect("Failed to parse type_cast as a type"); let StructInfo { - struct_name: name, + struct_name: enum_name, impl_generics, ty_generics, where_clause, - lifetime: _lifetime, .. - } = crate::helpers::extract_struct_info(&input, None); + } = extract_struct_info(&input, None); - let type_cast_handler = match type_cast_handler { - None => { - quote! { value } - } - Some(handler) => { - let handler = syn::parse_str::(&handler) + let cast_handler_expr = match type_cast_handler { + None => quote!(value), + Some(handler_str) => { + let handler_expr = syn::parse_str::(&handler_str) .expect("Failed to parse type_cast_handler"); - quote! { #handler } + quote!(#handler_expr) } }; + // Build match arms for each variant's discriminant (explicit or implicit). let enum_arms = if let syn::Data::Enum(data) = &input.data { - let mut next_discriminant = 0; + let mut next_disc = 0; data.variants .iter() .map(|variant| { - let variant_name = &variant.ident; - let discriminant = if let Some((_, expr)) = &variant.discriminant { - // Use the explicit discriminant - quote! { #expr } + let variant_ident = &variant.ident; + // If the variant has a discriminant (e.g., `Variant = 5`), use that. + // Otherwise, use the running `next_disc`. + let disc_expr = if let Some((_, disc)) = &variant.discriminant { + quote! { #disc } } else { - // Use the next implicit discriminant - let disc = quote! { #next_discriminant }; - next_discriminant += 1; - disc + let disc_token = quote! { #next_disc }; + next_disc += 1; + disc_token }; quote! { - #discriminant => Ok(#name::#variant_name), + #disc_expr => Ok(#enum_name::#variant_ident), } }) .collect::>() } else { - panic!("NetDecode with type_cast enabled can only be derived for enums."); + panic!("`#[net(type_cast = ...)]` is only valid on enums."); }; let expanded = quote! { - impl #impl_generics ferrumc_net_codec::decode::NetDecode for #name #ty_generics #where_clause { - fn decode(reader: &mut R, opts: &ferrumc_net_codec::decode::NetDecodeOpts) -> ferrumc_net_codec::decode::NetDecodeResult { - let value = <#type_cast as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?; - let value = #type_cast_handler; - let value = value as #repr_attr; + impl #impl_generics ferrumc_net_codec::decode::NetDecode + for #enum_name #ty_generics + #where_clause + { + fn decode( + reader: &mut R, + opts: &ferrumc_net_codec::decode::NetDecodeOpts + ) -> ferrumc_net_codec::decode::NetDecodeResult { + // Decode the initial numeric value + let value = <#type_cast_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?; + // Possibly transform via the handler + let value = #cast_handler_expr; + // Cast to the repr type + let value = value as #repr_ident; + + // Match against the known variant discriminants match (value as i32) { #(#enum_arms)* _ => Err(ferrumc_net_codec::decode::errors::NetDecodeError::InvalidEnumVariant), @@ -132,41 +131,126 @@ pub(crate) fn derive(input: TokenStream) -> TokenStream { } } }; - return TokenStream::from(expanded); } - let fields = if let syn::Data::Struct(data) = &input.data { - &data.fields - } else { - panic!("NetDecode can only be derived for structs or enums with u8_cast enabled."); - }; - - let decode_fields = fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); - let field_ty = &field.ty; - quote! { - #field_name: <#field_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?, - } - }); - + // Otherwise, handle struct decoding. We'll check if each field has an optional trigger. let StructInfo { struct_name, impl_generics, ty_generics, where_clause, - lifetime: _lifetime, .. - } = crate::helpers::extract_struct_info(&input, None); + } = extract_struct_info(&input, None); - let expanded = quote! { - // impl ferrumc_net_codec::decode::NetDecode for #name { - impl #impl_generics ferrumc_net_codec::decode::NetDecode for #struct_name #ty_generics #where_clause { - fn decode(reader: &mut R, opts: &ferrumc_net_codec::decode::NetDecodeOpts) -> ferrumc_net_codec::decode::NetDecodeResult { - Ok(Self { - #(#decode_fields)* + let fields = match &input.data { + syn::Data::Struct(data) => &data.fields, + _ => panic!("NetDecode can only be derived for structs or for enums with `u8_cast`."), + }; + + // Generate per-field decode statements. We'll build them in order, storing + // them in local variables named the same as the field, so the subsequent fields + // can use them in the optional triggers if needed. + let mut decode_statements = Vec::new(); + let mut field_names = Vec::new(); + + for field in fields { + let field_name = field + .ident + .clone() + .expect("Unnamed fields are not currently supported"); + let field_ty = &field.ty; + + // Check for optional trigger attribute: `#[net(optional_trigger = "...expr...")]` + // or something like `#[net(optional_trigger = { some_field == true })]`. + let mut optional_trigger_expr: Option = None; + + // Check the `net(...)` attributes on this field + for attr in &field.attrs { + if attr.path().is_ident("net") { + // e.g., #[net(optional_trigger = { some_field == true })] + + attr.parse_nested_meta(|meta| { + if let Some(ident) = meta.path.get_ident() { + if ident.to_string().as_str() == "optional_trigger" { + meta.parse_nested_meta(|meta| { + if let Some(expr) = meta.path.get_ident() { + let val = syn::parse_str::(&expr.to_string()) + .expect("Failed to parse optional_trigger expression"); + + optional_trigger_expr = Some(val); + } else { + panic!("Expected an expression for optional_trigger"); + } + + Ok(()) + }) + .expect("Failed to parse optional_trigger expression"); + } + } + Ok(()) + }) + .unwrap(); + } + } + + // Generate decoding code depending on whether there's an optional trigger + if let Some(expr) = optional_trigger_expr { + // For an optional field, we decode it only if `expr` is true at runtime. + // We'll store the result in a local variable `field_name` which will be an Option. + // Then at the end, we can build the struct using those local variables. + decode_statements.push(quote! { + let #field_name = { + if #expr { + Some(<#field_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?) + } else { + None + } + }; + }); + } else { + // Check if the field is an Option and handle it accordingly. + let is_optional = { + let ty_str = quote! { #field_ty }.to_string(); + ty_str.contains("Option<") + }; + + if is_optional { + decode_statements.push(quote! { + compile_error!("Optional fields must have an `optional_trigger` attribute\n\ + Example: #[net(optional_trigger = { some_field == true })]"); }) } + + // Normal (non-optional) field decode: + decode_statements.push(quote! { + let #field_name = <#field_ty as ferrumc_net_codec::decode::NetDecode>::decode(reader, opts)?; + }); + } + + field_names.push(field_name); + } + + // After decoding everything into local variables, construct the struct. + let build_struct = quote! { + Ok(Self { + #(#field_names),* + }) + }; + + let expanded = quote! { + impl #impl_generics ferrumc_net_codec::decode::NetDecode + for #struct_name #ty_generics + #where_clause + { + fn decode( + reader: &mut R, + opts: &ferrumc_net_codec::decode::NetDecodeOpts + ) -> ferrumc_net_codec::decode::NetDecodeResult { + #(#decode_statements)* + + #build_struct + } } }; diff --git a/src/lib/ecs/Cargo.toml b/src/lib/ecs/Cargo.toml index eb8ac550..b33e4980 100644 --- a/src/lib/ecs/Cargo.toml +++ b/src/lib/ecs/Cargo.toml @@ -9,4 +9,7 @@ thiserror = { workspace = true } dashmap = { workspace = true } parking_lot = { workspace = true } rayon = { workspace = true } -tracing = { workspace = true } \ No newline at end of file +tracing = { workspace = true } + +[dev-dependencies] +criterion = { workspace = true} \ No newline at end of file diff --git a/src/lib/ecs/benches/bench.rs b/src/lib/ecs/benches/bench.rs new file mode 100644 index 00000000..dc498388 --- /dev/null +++ b/src/lib/ecs/benches/bench.rs @@ -0,0 +1,100 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use ferrumc_ecs::Universe; + +#[allow(dead_code)] +struct Position { + x: f32, + y: f32, +} + +#[allow(dead_code)] +struct Velocity { + x: f32, + y: f32, +} + +fn create_entity(universe: &Universe) { + // entity is 0 here; + universe + .builder() + .with(Position { x: 0.0, y: 0.0 }) + .unwrap() + .build(); +} + +fn get_position_immut(universe: &Universe) { + let position = universe.get::(0).unwrap(); + assert_eq!(position.x, 0.0); + assert_eq!(position.y, 0.0); +} + +fn get_position_mut(universe: &Universe) { + let position = universe.get_mut::(0).unwrap(); + assert_eq!(position.x, 0.0); + assert_eq!(position.y, 0.0); +} + +fn _create_1000_entities_with_pos_and_vel(universe: &Universe) { + for i in 0..1000 { + let builder = universe + .builder() + .with(Position { + x: i as f32, + y: i as f32, + }) + .unwrap(); + if i % 2 == 0 { + builder + .with(Velocity { + x: i as f32, + y: i as f32, + }) + .unwrap(); + } + } +} + +fn query_10k_entities(universe: &Universe) { + let query = universe.query::<(&Position, &Velocity)>(); + for (_, (position, velocity)) in query { + assert_eq!(position.x, velocity.x); + assert_eq!(position.y, velocity.y); + } +} + +fn criterion_benchmark(c: &mut Criterion) { + let mut world = Universe::new(); + c.benchmark_group("entity") + .bench_function("create_entity", |b| { + b.iter(|| { + create_entity(black_box(&world)); + }); + // Create a new world after bench is done. + world = Universe::new(); + world + .builder() + .with(Position { x: 0.0, y: 0.0 }) + .unwrap() + .build(); + }) + .bench_function("get immut", |b| { + b.iter(|| { + get_position_immut(black_box(&world)); + }); + }) + .bench_function("get mut", |b| { + b.iter(|| { + get_position_mut(black_box(&world)); + }); + }) + .bench_function("query 10k entities", |b| { + let universe = Universe::new(); + _create_1000_entities_with_pos_and_vel(&universe); + b.iter(|| { + query_10k_entities(black_box(&world)); + }); + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/src/lib/events/src/infrastructure.rs b/src/lib/events/src/infrastructure.rs index 25f612ce..5f3e09b5 100644 --- a/src/lib/events/src/infrastructure.rs +++ b/src/lib/events/src/infrastructure.rs @@ -99,62 +99,6 @@ pub trait Event: Sized + Send + Sync + 'static { Ok(()) } - /*/// Trigger the execution of an event with concurrency support - /// - /// If the event structure supports cloning. This method can be used to execute - /// listeners of the same priority concurrently (using tokio::task). This imply a - /// cloning cost at each listener execution. See `Event::trigger` for a more - /// efficient but more linear approach. - /// - /// # Mutability policy - /// - /// The listeners having the same priority being runned concurrently, there are no - /// guarantees in the order of mutation of the event data. - /// - /// It is recommended to ensure listeners of the same priority exclusively update fields - /// in the event data that are untouched by other listeners of the same group. - async fn trigger_concurrently(event: Self::Data) -> Result<(), Self::Error> - where - Self::Data: Clone, - { - let read_guard = &EVENTS_LISTENERS; - let listeners = read_guard.get(Self::name()).unwrap(); - - // Convert listeners iterator into Stream - let mut stream = stream::iter(listeners.iter()); - - let mut priority_join_set = Vec::new(); - let mut current_priority = 0; - - while let Some(Some(listener)) = stream - .next() - .await - .map(|l| l.downcast_ref::>()) - { - if listener.priority == current_priority { - priority_join_set.push(tokio::spawn((listener.listener)(event.clone()))); - } else { - // Await over all listeners launched - let joined = future::join_all(priority_join_set.iter_mut()).await; - - // If one listener fail we return the first error - if let Some(err) = joined - .into_iter() - .filter_map(|res| res.expect("No task should ever panic. Impossible;").err()) - .next() - { - return Err(err); - } - - // Update priority to the new listener(s) - current_priority = listener.priority; - priority_join_set.push(tokio::spawn((listener.listener)(event.clone()))); - } - } - - Ok(()) - } - */ /// Register a new event listener for this event fn register(listener: AsyncEventListener, priority: u8) { // Create the event listener structure diff --git a/src/lib/net/crates/codec/src/net_types/network_position.rs b/src/lib/net/crates/codec/src/net_types/network_position.rs index dd138782..4a745b0a 100644 --- a/src/lib/net/crates/codec/src/net_types/network_position.rs +++ b/src/lib/net/crates/codec/src/net_types/network_position.rs @@ -40,6 +40,7 @@ impl NetEncode for NetworkPosition { _: &NetEncodeOpts, ) -> NetEncodeResult<()> { use tokio::io::AsyncWriteExt; + writer .write_all(self.as_u64().to_be_bytes().as_ref()) .await?; diff --git a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs index 39ca09c7..345cba66 100644 --- a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs +++ b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs @@ -7,7 +7,7 @@ use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_world::chunk_format::{Chunk, Heightmaps}; use std::io::{Cursor, Write}; use std::ops::Not; -use tracing::warn; +use tracing::{trace, warn}; const SECTIONS: usize = 24; // Number of sections, adjust for your Y range (-64 to 319) @@ -117,9 +117,11 @@ impl ChunkAndLightData { // If there is no palette entry, write a 0 (air) and log a warning None => { VarInt::new(0).write(&mut data)?; - warn!( + trace!( "No palette entry found for section at {}, {}, {}", - chunk.x, section.y, chunk.z + chunk.x, + section.y, + chunk.z ); } } diff --git a/src/lib/net/src/utils/broadcast.rs b/src/lib/net/src/utils/broadcast.rs index b21475d4..c67b068d 100644 --- a/src/lib/net/src/utils/broadcast.rs +++ b/src/lib/net/src/utils/broadcast.rs @@ -1,6 +1,7 @@ use crate::connection::StreamWriter; use crate::NetResult; use async_trait::async_trait; +use ferrumc_core::chunks::chunk_receiver::ChunkReceiver; use ferrumc_ecs::entities::Entity; use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts}; use ferrumc_state::GlobalState; @@ -64,10 +65,12 @@ impl BroadcastOptions { } fn get_all_entities(state: &GlobalState) -> HashSet { + // If it needs a chunk, then it's player!! :) + // !!!= === =.>>> if it works dont break it state .universe .get_component_manager() - .get_entities_with::() + .get_entities_with::() .into_iter() .collect() } diff --git a/src/lib/utils/general_purpose/src/paths/exe_path.rs b/src/lib/utils/general_purpose/src/paths/exe_path.rs new file mode 100644 index 00000000..d6ab1366 --- /dev/null +++ b/src/lib/utils/general_purpose/src/paths/exe_path.rs @@ -0,0 +1,43 @@ +use std::path::PathBuf; +use std::env::current_exe; + +#[derive(thiserror::Error, Debug)] +pub enum RootPathError { + #[error("Failed to get the current executable location.")] + IoError(#[from] std::io::Error), + #[error("Failed to get the parent directory of the executable.")] + NoParent, +} + +pub fn get_root_path() -> PathBuf { + // Since it should technically never fail. + // And if it fails, then it's a critical error, and the program should exit. + get_root_path_internal().unwrap() +} + +fn get_root_path_internal() -> Result { + //! Returns the root path of the executable. + //! e.g. + //! - If the executable is located at "D:/server/ferrumc.exe", + //! this function will return "D:/server". + //! + //! + //! # Errors + //! - If the current executable location cannot be found. (RootPathError::IoError) + //! - If the parent directory of the executable cannot be found. (RootPathError::NoParent) + //! + //! # Examples + //! ```rust + //! use ferrumc_general_purpose::paths::get_root_path; + //! + //! // Returns a Result + //! let root_path = get_root_path(); + //! + //! let favicon_path = root_path.join("icon.png"); + //! ``` + //! + let exe_location = current_exe()?; + let exe_dir = exe_location.parent().ok_or(RootPathError::NoParent)?; + + Ok(exe_dir.to_path_buf()) +} \ No newline at end of file