2026 · actif
PicoLimbo
Un serveur Minecraft Limbo réécrit en Rust from scratch, supportant les versions de la 1.7.2 à la dernière.

Overview
PicoLimbo est un serveur Minecraft réécrit en Rust from scratch, qui supporte les versions de la 1.7.2 à la dernière. Ce projet est un serveur Minecraft Limbo, c'est-à-dire que le proxy, lorsque le serveur principal est complet, ou éteint, redirige les joueurs vers ce serveur, où le joueur ne peut rien faire. Dans ce projet j'ai collaboré pour apprendre le langage Rust, et pour découvrir comment fonctionne vraiment un serveur Minecraft, et comment il communique avec les clients.
Ajout d'une hauteur minimum dans le monde
Vu que le serveur a pour but d'être un serveur Limbo, pour s'occuper, les serveurs construisent des jumps dans leur limbo, mais comme il n'y avait pas de hauteur minimum, les joueurs pouvaient tomber dans le vide, sans pouvoir remonter, et donc se retrouver bloqués dans le vide. Ma première contribution a été d'ajouter une hauteur minimum dans le monde, pour que les joueurs ne puissent pas tomber dans le vide, et donc se retrouver bloqués dans le vide.
Premièrement, j'ai dû comprendre comment fonctionne le serveur, pour ça j'ai vu que chaque action était gérée par un paquet spécifique.
J'ai dû premièrement enregistrer le paquet de mouvement du joueur, pour ça j'ai dû ajouter dans chaque version le paquet, avec son protocol id.
Voici l'exemple avec la version 1.21.6.
"serverbound": {
// Packet de mouvement de rotation du joueur. (yaw, pitch)
"minecraft:move_player_pos_rot": {
"protocol_id": 6
},
// Packet de mouvement du joueur. (x, y, z)
"minecraft:move_player_pos": {
"protocol_id": 4
}
}
Ensuite, pour les deux paquets minecraft:move_player_pos_rot and minecraft:move_player_pos, j'ai dû enregistrer leurs structures :
#[derive(PacketIn)]
pub struct SetPlayerPositionPacket {
pub x: f64,
pub feet_y: f64,
pub z: f64,
[#pvn(769..)]
// Depuis la version 1.21.4, le packet contient des flags : 0x01, on_ground, 0x02 : pousser contre un mur
pub v_1_21_4_flags: u8,
// Avant la version 1.21.4, le packet contient un champ on_ground.
[#pvn(..769)]
pub on_ground: bool,
}
impl SetPlayerPositionPacket {
pub fn position(&self) -> (f64, f64, f64) {
(self.x, self.feet_y, self.z)
}
}
Puis, j'ai dû ajouter la logique pour vérifier si le joueur est en dessous de la hauteur minimum, et si c'est le cas, le téléporter à la hauteur minimum.
impl PacketHandler for SetPlayerPositionPacket {
fn handle(&self, client_state: &mut ClientState, server_state: &ServerState) {
// On verifie si le joueur est en dessous de la hauteur minimum
if self.feet_y < 12.0 {
// On le teleporte a la hauteur minimum
client_state.teleport_to(
self.x,
12.0,
self.z,
self.yaw,
self.pitch,
);
}
Ok(())
}
}
On fait la même chose pour le paquet minecraft:move_player_pos_rot.
Enfin, les paquets, il faut les enregistrer dans le serveur pour qu'il puisse les comprendre.
#[derive(PacketReport)]
pub enum PacketRegistry {
// ... Les autres packets
#[protocol_id(
state = "play",
bound = "serverbound",
name = "minecraft:move_player_pos",
)]
SetPlayerPosition(SetPlayerPositionPacket),
#[protocol_id(
state = "play",
bound = "serverbound",
name = "minecraft:move_player_pos_rot",
)]
SetPlayerPosition(SetPlayerPositionAndRotationPacket),
}
impl PacketHandler for PacketRegistry {
fn handle(&self, client_state: &mut ClientState, server_state: &ServerState) {
match self {
// ... Les autres packets
Self::SetPlayerPosition(packet) => packet.handle(client_state, server_state),
Self::SetPlayerPositionAndRotation(packet) => packet.handle(client_state, server_state),
_ => Err(PacketHandlerError::custom("Unhandled packet")),
}
}
}
Maintenant, il faut que le serveur puisse être configurable, pour ça, PicoLimbo contient un fichier de configuration.
Donc dans la configuration des experimental world config j'ai fait :
#[derive(Serialize, Deserialize, Default)]
pub struct ExperimentalConfig {
pub world: ExperimentalWorldConfig,
}
#[derive(Serialize, Deserialize)]
pub struct ExperimentalWorldConfig {
pub spawn_position: (f64, f64, f64),
pub view_distance: i32,
pub schematic_file: String,
pub min_y_pos: i32,
pub min_y_message: String,
}
impl Default for ExperimentalWorldConfig {
fn default() -> Self {
Self {
spawn_position: (0.0, 320.0, 0.0),
view_distance: 2,
schematic_file: String::new(),
min_y_pos: -64,
min_y_message: "You have reached the bottom of the world.".into(),
}
}
}
Maintenant, il faut faire une fonction pour simplifier l'envoi de messages au joueur, dans toutes les versions. Donc dans le client_state.rs j'ai rajouté :
impl ClientState {
pub fn send_message(&mut self, message: String) {
if self
.protocol_version
.is_after_inclusive(ProtocolVersion::V1_19)
{
let packet = SystemChatMessagePacket::plain_text(message);
self.queue_packet(PacketRegistry::SystemChatMessage(packet));
} else {
let packet = LegacyChatMessagePacket::system(message);
self.queue_packet(PacketRegistry::LegacyChatMessage(packet));
}
}
}
Enfin, plus qu'à rajouter une fonction pour renvoyer le joueur au spawn et lui envoyer un message.
pub fn teleport_player_to_spawn(client_state: &mut ClientState, server_state: &ServerState) {
let (x, y, z) = server_state.spawn_position();
let packet = SynchronizePlayerPositionPacket::new(x, y, z);
client_state.queue_packet(PacketRegistry::SynchronizePlayerPosition(packet));
if let Some(content) = server_state.min_y_message() {
client_state.send_message(content);
}
}
Puis, dans les paquets SetPlayerPositionPacket et SetPlayerPositionAndRotationPacket, j'ai rajouté un appel à cette fonction si le joueur est en dessous de la hauteur minimum.
impl PacketHandler for SetPlayerPositionPacket {
fn handle(&self, client_state: &mut ClientState, server_state: &ServerState) {
// On recupere la hauteur minimum du serveur.
let min_y_pos_config = server_state.min_y_pos();
// On verifie si le joueur est en dessous de la hauteur minimum
if self.feet_y < f64::from(min_y_pos_config) {
teleport_player_to_spawn(client_state, server_state);
}
Ok(())
}
}