Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ members = [
"src/lib/physics",
"src/lib/performance",
"src/lib/dashboard",
"src/lib/particles",
]

[workspace.package]
Expand Down Expand Up @@ -129,6 +130,7 @@ ferrumc-dashboard = { path = "src/lib/dashboard" }
ferrumc-messages = { path = "src/lib/messages" }
ferrumc-entities = { path = "src/lib/entities" }
ferrumc-physics = { path = "src/lib/physics" }
ferrumc-particles = { path = "src/lib/particles" }

# Asynchronous
tokio = { version = "1.48.0", features = [
Expand Down Expand Up @@ -156,7 +158,7 @@ crossbeam-queue = "0.3.12"

# Network
ureq = "3.1.4"
reqwest = { version = "0.12.28", features = ["json"] }
reqwest = { version = "0.13.1", features = ["json"] }

# Error handling
thiserror = "2.0.17"
Expand All @@ -174,7 +176,7 @@ num-bigint = "0.4.6"

# Encoding/Serialization
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.146"
serde_json = "1.0.148"
serde_derive = "1.0.228"
base64 = "0.22.1"

Expand All @@ -197,11 +199,12 @@ bimap = "0.6.3"
# Macros
lazy_static = "1.5.0"
quote = "1.0.42"
syn = "2.0.111"
proc-macro2 = "1.0.103"
syn = "2.0.112"
proc-macro2 = "1.0.104"
paste = "1.0.15"
maplit = "1.0.2"
macro_rules_attribute = "0.2.2"
enum_discriminant = "1.0.1"

# Magic
dhat = "0.3.3"
Expand All @@ -214,7 +217,6 @@ yazi = "0.2.1"

# Database
heed = "0.22.1-nested-rtxns-6"
moka = "0.12.12"

# CLI
clap = "4.5.53"
Expand Down Expand Up @@ -249,3 +251,6 @@ axum = { version = "0.8.7", features = ["tokio", "ws"]}

# Stats
sysinfo = "0.37.2"

[profile.dev.package.yazi]
opt-level = 3
2 changes: 1 addition & 1 deletion assets/data/configs/main-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,4 @@ cache_capacity = 20_000
port = 9000
# Generated using:
# python -c "import secret;print(secrets.token_urlsafe())"
secret = "H4AiheIDL2-e2dM3mxSoHOhNRSyfGw1iGYlV3WcuaRw"
secret = "H4AiheIDL2-e2dM3mxSoHOhNRSyfGw1iGYlV3WcuaRw"
1 change: 1 addition & 0 deletions src/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ ferrumc-inventories = { workspace = true }
ferrumc-data = { workspace = true }
ferrumc-entities = { workspace = true }
ferrumc-physics = { workspace = true }
ferrumc-particles = { workspace = true }
bevy_math = { workspace = true }
ferrumc-dashboard = { workspace = true, optional = true }
once_cell = { workspace = true }
Expand Down
16 changes: 16 additions & 0 deletions src/bin/src/game_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::register_messages::register_messages;
use crate::register_resources::register_resources;
use crate::systems::lan_pinger::LanPinger;
use crate::systems::listeners::register_gameplay_listeners;
use crate::systems::mobs::register_mob_systems;
use crate::systems::physics::register_physics;
use crate::systems::register_game_systems;
use crate::systems::shutdown_systems::register_shutdown_systems;
Expand Down Expand Up @@ -252,6 +253,7 @@ fn build_timed_scheduler() -> Scheduler {
register_game_systems(s); // General game logic
register_gameplay_listeners(s); // Event listeners for gameplay events
register_physics(s); // Physics systems (movement, collision, etc.)
register_mob_systems(s); // Mob AI and behavior
};
let tick_period = Duration::from_secs(1) / get_global_config().tps;
timed.register(
Expand All @@ -273,6 +275,20 @@ fn build_timed_scheduler() -> Scheduler {
.with_behavior(MissedTickBehavior::Skip),
);

// -------------------------------------------------------------------------
// CHUNK GC SCHEDULE - Periodic chunk garbage collection
// -------------------------------------------------------------------------
//
// Cleans up unused chunks from memory to free resources.
// Uses Skip behavior - if we miss a GC, just wait for the next one.
let build_chunk_gc = |s: &mut Schedule| {
s.add_systems(crate::systems::chunk_unloader::handle);
};
timed.register(
TimedSchedule::new("chunk_gc", Duration::from_secs(5), build_chunk_gc)
.with_behavior(MissedTickBehavior::Skip),
);

// -------------------------------------------------------------------------
// KEEPALIVE SCHEDULE - Prevents client timeout disconnects
// -------------------------------------------------------------------------
Expand Down
8 changes: 2 additions & 6 deletions src/bin/src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ use ferrumc_threadpool::ThreadPool;
use ferrumc_world::pos::ChunkPos;
use ferrumc_world::World;
use ferrumc_world_gen::WorldGenerator;
use std::sync::Arc;
use std::time::Instant;
use tracing::{error, info};

Expand Down Expand Up @@ -46,14 +45,11 @@ pub fn generate_spawn_chunks(state: GlobalState) -> Result<(), BinaryError> {
let state_clone = state.clone();
batch.execute(move || {
let pos = ChunkPos::new(x, z);
let chunk = state_clone
.terrain_generator
.generate_chunk(pos)
.map(Arc::new);
let chunk = state_clone.terrain_generator.generate_chunk(pos);

match chunk {
Ok(chunk) => {
if let Err(e) = state_clone.world.save_chunk(pos, "overworld", chunk) {
if let Err(e) = state_clone.world.insert_chunk(pos, "overworld", chunk) {
error!("Error saving chunk ({}, {}): {:?}", x, z, e);
}
}
Expand Down
26 changes: 6 additions & 20 deletions src/bin/src/packet_handlers/play_packets/place_block.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::sync::Arc;

use bevy_ecs::prelude::{Entity, Query, Res};
use ferrumc_core::collisions::bounds::CollisionBounds;
use ferrumc_core::transform::position::Position;
Expand Down Expand Up @@ -72,13 +70,12 @@ pub fn handle(
item_id.0, mapped_block_state_id
);
let pos: BlockPos = event.position.into();
let mut chunk = match state.0.world.load_chunk_owned(pos.chunk(), "overworld") {
Ok(chunk) => chunk,
Err(e) => {
debug!("Failed to load chunk: {:?}", e);
continue 'ev_loop;
}
};
let mut chunk = ferrumc_utils::world::load_or_generate_mut(
&state.0,
pos.chunk(),
"overworld",
)
.expect("Failed to load or generate chunk");
let Ok(block_clicked) = chunk.get_block(pos.chunk_block_pos()) else {
debug!("Failed to get block at position: {}", pos);
continue 'ev_loop;
Expand Down Expand Up @@ -154,17 +151,6 @@ pub fn handle(
error!("Failed to send block change ack packet: {:?}", err);
continue 'ev_loop;
}

if let Err(err) =
state
.0
.world
.save_chunk(pos.chunk(), "overworld", Arc::from(chunk))
{
error!("Failed to save chunk after block placement: {:?}", err);
} else {
trace!("Block placed at {}", offset_pos);
}
}
}
1 => {
Expand Down
44 changes: 16 additions & 28 deletions src/bin/src/packet_handlers/play_packets/player_action.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
use std::sync::Arc;

use crate::errors::BinaryError;
use bevy_ecs::prelude::{Entity, MessageWriter, Query, Res};
use ferrumc_components::player::abilities::PlayerAbilities;
use ferrumc_messages::player_digging::*;
use ferrumc_messages::BlockBrokenEvent;

use ferrumc_net::connection::StreamWriter;
use ferrumc_net::packets::outgoing::block_change_ack::BlockChangeAck;
Expand All @@ -12,16 +11,19 @@ use ferrumc_net::PlayerActionReceiver;
use ferrumc_net_codec::net_types::var_int::VarInt;
use ferrumc_state::GlobalStateResource;
use ferrumc_world::{block_state_id::BlockStateId, pos::BlockPos};
use tracing::{error, trace, warn};
use tracing::{error, warn};

pub fn handle(
receiver: Res<PlayerActionReceiver>,
state: Res<GlobalStateResource>,
broadcast_query: Query<(Entity, &StreamWriter)>,
player_query: Query<&PlayerAbilities>,
mut start_dig_events: MessageWriter<PlayerStartedDigging>,
mut cancel_dig_events: MessageWriter<PlayerCancelledDigging>,
mut finish_dig_events: MessageWriter<PlayerFinishedDigging>,
(mut start_dig_events, mut cancel_dig_events, mut finish_dig_events, mut block_break_events): (
MessageWriter<PlayerStartedDigging>,
MessageWriter<PlayerCancelledDigging>,
MessageWriter<PlayerFinishedDigging>,
MessageWriter<BlockBrokenEvent>,
),
) {
// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol?oldid=2773393#Player_Action
for (event, trigger_eid) in receiver.0.try_iter() {
Expand All @@ -40,32 +42,18 @@ pub fn handle(
// Only instabreak (status 0) is relevant in creative.
if event.status.0 == 0 {
let res: Result<(), BinaryError> = try {
let mut chunk = match state
.0
.clone()
.world
.load_chunk_owned(pos.chunk(), "overworld")
{
Ok(chunk) => chunk,
Err(e) => {
trace!("Chunk not found, generating new chunk: {:?}", e);
state
.0
.clone()
.terrain_generator
.generate_chunk(pos.chunk())
.map_err(BinaryError::WorldGen)?
}
};
let mut chunk = ferrumc_utils::world::load_or_generate_mut(
&state.0,
pos.chunk(),
"overworld",
)
.expect("Failed to load or generate chunk");
chunk
.set_block(pos.chunk_block_pos(), BlockStateId::default())
.map_err(BinaryError::World)?;

state
.0
.world
.save_chunk(pos.chunk(), "overworld", Arc::new(chunk))
.map_err(BinaryError::World)?;
// Send block broken event for un-grounding system
block_break_events.write(BlockBrokenEvent { position: pos });

// Broadcast the change
for (eid, conn) in &broadcast_query {
Expand Down
9 changes: 6 additions & 3 deletions src/bin/src/register_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use ferrumc_commands::messages::{CommandDispatched, ResolvedCommandDispatched};
use ferrumc_core::conn::force_player_recount_event::ForcePlayerRecount;
use ferrumc_messages::chunk_calc::ChunkCalc;
use ferrumc_messages::entity_update::SendEntityUpdate;
use ferrumc_messages::particle::SendParticle;
use ferrumc_messages::{
PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating, PlayerFinishedDigging,
PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft, PlayerLeveledUp,
PlayerStartedDigging, SpawnEntityCommand, SpawnEntityEvent,
BlockBrokenEvent, PlayerCancelledDigging, PlayerDamaged, PlayerDied, PlayerEating,
PlayerFinishedDigging, PlayerGainedXP, PlayerGameModeChanged, PlayerJoined, PlayerLeft,
PlayerLeveledUp, PlayerStartedDigging, SpawnEntityCommand, SpawnEntityEvent,
};
use ferrumc_net::packets::packet_messages::Movement;

Expand All @@ -32,4 +33,6 @@ pub fn register_messages(world: &mut World) {
MessageRegistry::register_message::<SpawnEntityCommand>(world);
MessageRegistry::register_message::<SpawnEntityEvent>(world);
MessageRegistry::register_message::<SendEntityUpdate>(world);
MessageRegistry::register_message::<SendParticle>(world);
MessageRegistry::register_message::<BlockBrokenEvent>(world);
}
28 changes: 18 additions & 10 deletions src/bin/src/systems/chunk_calculator.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use bevy_ecs::prelude::{MessageReader, Query};
use bevy_math::IVec2;
use ferrumc_config::server_config::get_global_config;
use ferrumc_core::chunks::chunk_receiver::ChunkReceiver;
use ferrumc_core::transform::position::Position;
use ferrumc_messages::chunk_calc::ChunkCalc;
use ferrumc_world::pos::ChunkPos;

pub fn handle(
mut messages: MessageReader<ChunkCalc>,
Expand All @@ -14,17 +16,14 @@ pub fn handle(
Err(_) => continue, // Skip if the player does not exist
};

let chunk_receiver = &mut *chunk_receiver;

let radius = get_global_config().chunk_render_distance as i32;
let player_chunk_x = position.x.floor() as i32 >> 4;
let player_chunk_z = position.z.floor() as i32 >> 4;
let player_chunk = ChunkPos::from(position.coords);

let mut queued_chunks = Vec::new();

// Add all chunks within the radius to the loading list if not already loaded
for x in player_chunk_x - radius..=player_chunk_x + radius {
for z in player_chunk_z - radius..=player_chunk_z + radius {
for x in player_chunk.x() - radius..=player_chunk.x() + radius {
for z in player_chunk.z() - radius..=player_chunk.z() + radius {
let chunk_coords = (x, z);
if !chunk_receiver.loaded.contains(&chunk_coords) {
queued_chunks.push(chunk_coords);
Expand All @@ -34,15 +33,24 @@ pub fn handle(

// Sort loading list to prioritize closer chunks
queued_chunks.sort_by_key(|(x, z)| {
let dx = x - player_chunk_x;
let dz = z - player_chunk_z;
dx * dx + dz * dz
let as_vec = IVec2::new(*x, *z);
as_vec.chebyshev_distance(IVec2::new(player_chunk.x(), player_chunk.z()))
});

for coords in queued_chunks {
chunk_receiver.loading.push_back(coords);
}

// TODO: Handle unloading of distant chunks
// Unload chunks that are outside the radius

for loaded_chunk in chunk_receiver.loaded.clone() {
let vec = IVec2::new(loaded_chunk.0, loaded_chunk.1);
let dx = IVec2::new(player_chunk.x(), player_chunk.z()).chebyshev_distance(vec);
if dx > radius as u32 {
if let Some(pos) = chunk_receiver.loaded.take(&loaded_chunk) {
chunk_receiver.unloading.push_back(pos);
}
}
}
}
}
Loading