Skip to content
Open
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
3 changes: 2 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: Rust CI
on:
pull_request:
branches: [ "master" ]
types: [ opened, synchronize, reopened, ready_for_review ]
push:
branches: [ "master" ]
workflow_dispatch:
Expand Down Expand Up @@ -112,4 +113,4 @@ jobs:
tool: cargo-nextest

- name: Run Tests
run: cargo nextest run --target ${{ matrix.target }} --all-targets --all-features -E "not kind(bench)"
run: cargo nextest run --target ${{ matrix.target }} --all-targets --all-features -E "not kind(bench)"
20 changes: 10 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ temper-game-systems = { path = "src/game_systems" }
interactions = { path = "src/game_systems/src/interactions" }

# Asynchronous
tokio = { version = "1.50.0", features = [
tokio = { version = "1.51.1", features = [
"macros",
"net",
"rt",
Expand Down Expand Up @@ -197,14 +197,14 @@ sha1 = "0.11.0"
num-bigint = "0.4.6"

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

bitcode = "0.6.9"
bitcode_derive = "0.6.9"
toml = "1.1.0+spec-1.1.0"
toml = "1.1.2+spec-1.1.0"
craftflow-nbt = "2.1.0"
figment = { version = "0.10.19", features = ["toml", "env"] }
simd-json = "0.17.0"
Expand All @@ -215,9 +215,9 @@ serde_yaml_ng = "0.10.0"
byteorder = "1.5.0"

# Data types
dashmap = "7.0.0-rc2"
dashmap = { version = "7.0.0-rc2", features = ["serde"] }
uuid = { version = "1.23.0", features = ["v4", "v3", "serde"] }
indexmap = { version = "2.13.0", features = ["serde"] }
indexmap = { version = "2.14.0", features = ["serde"] }
bimap = "0.6.3"

# Macros
Expand All @@ -232,14 +232,14 @@ type_hash = "0.3.0"

# Magic
dhat = "0.3.3"
ctor = "0.8.0"
ctor = "0.9.1"

# Compression/Decompression
yazi = "0.2.1"
flate2 = "1.1.9"

# Database
heed = "0.22.1-nested-rtxns-7"
heed = "0.22.1"

# Misc
deepsize = "0.2.0"
Expand All @@ -251,16 +251,16 @@ ctrlc = "3.5.2"
num_cpus = "1.17.0"
typename = "0.1.2"
bevy_ecs = { version = "0.18.1", features = ["multi_threaded", "trace", "debug"], default-features = false }
bevy_math = "0.18.1"
bevy_math = { version = "0.18.1", features = ["serialize"] }
once_cell = "1.21.4"
mime_guess = "2.0.5"

## TUI/CLI
crossterm = "0.29.0"
ratatui-core = "0.1.0"
tui-input = "0.15.0"
tui-input = "0.15.1"
ratatui = "0.30.0"
tui-logger = { version = "0.18.1", features = ["tracing-support", "crossterm"] }
tui-logger = { version = "0.18.2", features = ["tracing-support", "crossterm"] }
clap = { version = "4.6.0", features = ["derive", "env"] }
indicatif = "0.18.4"
colored = "3.1.1"
Expand Down
109 changes: 6 additions & 103 deletions src/app/runtime/src/game_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,18 @@
use crate::errors::BinaryError;
use crate::tui;
use bevy_ecs::prelude::World;
use bevy_ecs::schedule::{ExecutorKind, Schedule};
use bevy_ecs::schedule::Schedule;
use crossbeam_channel::Sender;
use std::sync::Arc;
use std::time::{Duration, Instant};
use temper_commands::infrastructure::register_command_systems;
use temper_config::server_config::get_global_config;
use temper_game_systems::{
LanPinger, chunk_unloader, keep_alive_system, register_background_systems,
register_mob_systems, register_packet_handlers, register_physics_systems,
register_player_systems, register_shutdown_systems, register_world_systems, update_player_ping,
world_sync,
};
use temper_game_systems::{LanPinger, register_schedules};
use temper_messages::register_messages;
use temper_net_runtime::connection::{NewConnection, handle_connection};
use temper_net_runtime::server::create_server_listener;
use temper_performance::tick::TickData;
use temper_protocol::{PacketSender, create_packet_senders};
use temper_resources::register_resources;
use temper_scheduler::MissedTickBehavior;
use temper_scheduler::{Scheduler, TimedSchedule, drain_registered_schedules};
use temper_scheduler::Scheduler;
use temper_state::{GlobalState, GlobalStateResource};
use temper_utils::formatting::format_duration;
use tracing::{Instrument, debug, error, info, info_span, trace, warn};
Expand Down Expand Up @@ -93,11 +85,9 @@ pub fn start_game_loop(global_state: GlobalState, no_tui: bool) -> Result<(), Bi
server_command_rx,
);

// Build the timed scheduler with all periodic schedules (tick, sync, keepalive)
let mut timed = build_timed_scheduler();

// Register systems that run on shutdown (save world, disconnect players, etc.)
register_shutdown_systems(&mut shutdown_schedule);
// Build the timed scheduler with all periodic schedules and shutdown systems.
let mut timed = Scheduler::new();
register_schedules(&mut timed, &mut shutdown_schedule);

// =========================================================================
// PHASE 4: Start Network Thread
Expand Down Expand Up @@ -238,93 +228,6 @@ pub fn start_game_loop(global_state: GlobalState, no_tui: bool) -> Result<(), Bi
Ok(())
}

/// Builds the timed scheduler with all periodic game schedules.
///
/// Each schedule runs at a specific interval and handles different aspects of the game:
/// - **tick**: Main game tick (player updates, packets, commands) - runs at configured TPS
/// - **world_sync**: Persists world data to disk - every 15 seconds
/// - **keepalive**: Sends keepalive packets to prevent timeouts - every 1 second
fn build_timed_scheduler() -> Scheduler {
let mut timed = Scheduler::new();

// -------------------------------------------------------------------------
// TICK SCHEDULE - Main game loop tick
// -------------------------------------------------------------------------
// This is the core game tick that runs at the configured TPS (ticks per second).
// It processes packets, updates players, handles commands, and runs game systems.
// Uses Burst behavior to catch up if ticks are missed (up to 5 at a time).
let build_tick = |s: &mut Schedule| {
s.set_executor_kind(ExecutorKind::SingleThreaded);
register_packet_handlers(s); // Handle incoming packets from players
register_player_systems(s); // Update player state (position, inventory, etc.)
register_command_systems(s); // Process queued commands

register_background_systems(s); // Systems that run in the background (day cycle, chunk sending, etc.)
register_physics_systems(s); // Physics systems (movement, collision, etc.)
register_mob_systems(s); // Mob AI and behavior
register_world_systems(s); // World updates (block changes, redstone, etc.)
};
let tick_period = Duration::from_secs(1) / get_global_config().tps;
timed.register(
TimedSchedule::new("tick", tick_period, build_tick)
.with_behavior(MissedTickBehavior::Burst) // Run missed ticks to catch up
.with_max_catch_up(5), // But only catch up 5 ticks max at once
);

// -------------------------------------------------------------------------
// WORLD SYNC SCHEDULE - Periodic world persistence
// -------------------------------------------------------------------------
// Saves the world state to disk periodically to prevent data loss.
// Uses Skip behavior - if we miss a sync, just wait for the next one.
let build_world_sync = |s: &mut Schedule| {
s.add_systems(world_sync::sync_world);
};
timed.register(
TimedSchedule::new("world_sync", Duration::from_secs(15), build_world_sync)
.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(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
// -------------------------------------------------------------------------
// Sends keepalive packets to all connected players to maintain the connection.
// Has a 250ms phase offset to spread load away from tick boundaries.
// Also handles updating player ping values.
let build_keepalive = |s: &mut Schedule| {
s.add_systems(keep_alive_system::keep_alive_system);
s.add_systems(update_player_ping::handle);
};
timed.register(
TimedSchedule::new("keepalive", Duration::from_secs(1), build_keepalive)
.with_behavior(MissedTickBehavior::Skip)
.with_phase(Duration::from_millis(250)), // Offset from tick schedule
);

// -------------------------------------------------------------------------
// PLUGIN SCHEDULES - Dynamically registered by plugins
// -------------------------------------------------------------------------
// Drain any schedules that plugins registered during initialization.
for pending in drain_registered_schedules() {
timed.register(pending.into_timed());
}

timed
}

/// Spawns the LAN broadcast pinger task.
///
/// This broadcasts the server's presence on the local network using UDP multicast
Expand Down
2 changes: 2 additions & 0 deletions src/components/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ temper-data = { workspace = true }
bevy_math = { workspace = true }
uuid = { workspace = true }
type_hash = { workspace = true }
serde = { workspace = true }
crossbeam-queue = { workspace = true }
3 changes: 2 additions & 1 deletion src/components/src/combat.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::metadata::EntityMetadata;
use bevy_ecs::prelude::Component;
use serde::{Deserialize, Serialize};
use temper_data::generated::entities::EntityType as VanillaEntityType;

/// Combat properties for an entity.
Expand All @@ -20,7 +21,7 @@ use temper_data::generated::entities::EntityType as VanillaEntityType;
/// combat.set_invulnerable(10); // 10 ticks of invulnerability
/// assert!(!combat.can_be_damaged());
/// ```
#[derive(Component, Clone, Copy)]
#[derive(Component, Clone, Copy, Serialize, Deserialize)]
pub struct CombatProperties {
/// True if an entity is attackable
///
Expand Down
3 changes: 2 additions & 1 deletion src/components/src/entity_identity.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bevy_ecs::prelude::Component;
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicI32, Ordering};

/// Global entity ID counter for non-player entities.
Expand All @@ -18,7 +19,7 @@ static ENTITY_ID_COUNTER: AtomicI32 = AtomicI32::new(1_000_000);
/// let pig_identity = Identity::new(Some("Pig".to_string()));
/// assert!(pig_identity.entity_id >= 1_000_000);
/// ```
#[derive(Debug, Component, Clone)]
#[derive(Debug, Component, Clone, Serialize, Deserialize)]
pub struct Identity {
/// Network entity ID used in packets.
/// Must be unique across all entities in the server.
Expand Down
11 changes: 11 additions & 0 deletions src/components/src/last_chunk_pos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use bevy_ecs::prelude::Component;
use temper_core::pos::ChunkPos;

#[derive(Component, Clone, Copy, Debug, Eq, PartialEq)]
pub struct LastChunkPos(pub ChunkPos);

impl LastChunkPos {
pub fn new(chunk: ChunkPos) -> Self {
Self(chunk)
}
}
3 changes: 2 additions & 1 deletion src/components/src/last_synced_position.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use crate::player::position::Position;
use bevy_ecs::prelude::Component;
use bevy_math::DVec3;
use serde::{Deserialize, Serialize};

/// Component that tracks the last position synchronized to clients
#[derive(Component, Debug, Clone, Copy)]
#[derive(Component, Debug, Clone, Copy, Serialize, Deserialize)]
pub struct LastSyncedPosition(pub DVec3);

impl LastSyncedPosition {
Expand Down
1 change: 1 addition & 0 deletions src/components/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod player;

// Core entity components based on temper-data
pub mod combat;
pub mod last_chunk_pos;
pub mod last_synced_position;
pub mod metadata;
pub mod physical;
Expand Down
3 changes: 2 additions & 1 deletion src/components/src/physical.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use bevy_ecs::prelude::Component;
use bevy_math::bounding::Aabb3d;
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
use temper_data::generated::entities::EntityType as VanillaEntityType;

/// Entity bounding box (collision box).
///
/// Represents the volume occupied by an entity in the world.
/// Used for collision detection and physics.
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct BoundingBox {
aabb: Aabb3d,
}
Expand Down
19 changes: 19 additions & 0 deletions src/components/src/player/entity_tracker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use bevy_ecs::entity::EntityHashSet;
use bevy_ecs::prelude::{Component, Entity};
use crossbeam_queue::SegQueue;
use uuid::Uuid;

/// Tracks entities that a player should start tracking, as well as entities that are currently being
/// tracked and entities that should be untracked. To track an entity, add its UUID and **entity type ID** to the `to_track` queue.
/// To untrack an entity, add its Entity to the `to_untrack` queue. The `tracking` set contains the ECS Entity IDs of currently tracked entities.
///
/// This component has several uses:
/// - It allows the server to keep track of which entities a player should be aware of and send the appropriate spawn and destroy packets.
/// - It can be used to optimize entity updates by only sending updates for entities that are currently being tracked by the player.
/// - It can be used to manage entity visibility and interactions, ensuring that players only interact with entities they are supposed to be aware of.
#[derive(Component, Default)]
pub struct EntityTracker {
pub to_track: SegQueue<(Uuid, u16)>,
pub tracking: EntityHashSet,
pub to_untrack: SegQueue<Entity>,
}
3 changes: 2 additions & 1 deletion src/components/src/player/grounded.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy_ecs::prelude::Component;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Component, Copy, Clone)]
#[derive(Debug, Default, Component, Copy, Clone, Serialize, Deserialize)]
pub struct OnGround(pub bool);

impl From<bool> for OnGround {
Expand Down
1 change: 1 addition & 0 deletions src/components/src/player/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod abilities;
pub mod chunk_receiver;
pub mod client_information;
pub mod entity_tracker;
pub mod experience;
pub mod gamemode;
pub mod gameplay_state;
Expand Down
2 changes: 2 additions & 0 deletions src/components/src/player/player_bundle.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::bounds::CollisionBounds;
use crate::entity_identity::Identity;
use crate::player::chunk_receiver::ChunkReceiver;
use crate::player::entity_tracker::EntityTracker;
use crate::player::grounded::OnGround;
use crate::player::player_marker::PlayerMarker;
use crate::player::player_properties::PlayerProperties;
Expand Down Expand Up @@ -36,6 +37,7 @@ pub struct PlayerBundle {
pub on_ground: OnGround,
pub chunk_receiver: ChunkReceiver,
pub collision_bounds: CollisionBounds,
pub entity_tracker: EntityTracker,

// Inventory
pub inventory: Inventory,
Expand Down
3 changes: 2 additions & 1 deletion src/components/src/player/position.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use bevy_ecs::prelude::Component;
use bevy_math::DVec3;
use serde::{Deserialize, Serialize};
use std::ops::DerefMut;
use std::{
fmt::{Debug, Display, Formatter},
Expand All @@ -8,7 +9,7 @@ use std::{
use temper_codec::net_types::network_position::NetworkPosition;
use temper_core::pos::ChunkPos;

#[derive(Component, Clone, Copy)]
#[derive(Component, Clone, Copy, Serialize, Deserialize)]
pub struct Position {
pub coords: DVec3,
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/src/player/rotation.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use bevy_ecs::prelude::Component;
use bitcode_derive::{Decode, Encode};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use type_hash::TypeHash;

#[derive(Component, Clone, Copy, Default, Decode, Encode, TypeHash)]
#[derive(Component, Clone, Copy, Default, Decode, Encode, TypeHash, Serialize, Deserialize)]
pub struct Rotation {
pub yaw: f32,
pub pitch: f32,
Expand Down
Loading