Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
/target
node_modules/
*.zst
.DS_Store
62 changes: 62 additions & 0 deletions crates/minecraft_packets/src/play/commands_packet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ pub struct CommandsPacket {

pub enum CommandArgumentType {
Float { min: f32, max: f32 },
Integer { min: i32, max: i32 },
String { behavior: StringBehavior },
}

#[repr(i8)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum StringBehavior {
SingleWord = 0,
QuotablePhrase = 1,
GreedyPhrase = 2,
}

pub struct CommandArgument {
Expand All @@ -37,6 +47,20 @@ impl CommandArgument {
argument_type: CommandArgumentType::Float { min, max },
}
}

pub fn integer(name: impl ToString, min: i32, max: i32) -> Self {
Self {
name: name.to_string(),
argument_type: CommandArgumentType::Integer { min, max },
}
}

pub fn string(name: impl ToString, behavior: StringBehavior) -> Self {
Self {
name: name.to_string(),
argument_type: CommandArgumentType::String { behavior },
}
}
}

pub struct Command {
Expand Down Expand Up @@ -81,6 +105,10 @@ impl CommandsPacket {

let properties = match argument.argument_type {
CommandArgumentType::Float { min, max } => ParserProperties::float(min, max),
CommandArgumentType::Integer { min, max } => {
ParserProperties::integer(min, max)
}
CommandArgumentType::String { behavior } => ParserProperties::string(behavior),
};

nodes.push(Node::argument(argument.name, properties));
Expand Down Expand Up @@ -192,18 +220,32 @@ enum ParserProperties {
/// Only if flags & 0x02. If not specified, defaults to Float.MAX_VALUE (≈ 3.4028235E38)
max: Omitted<f32>,
},
Integer {
flags: i8,
/// Only if flags & 0x01. If not specified, defaults to Integer.MIN_VALUE (2147483648)
min: Omitted<i32>,
/// Only if flags & 0x02. If not specified, defaults to Integer.MAX_VALUE (-2147483647)
max: Omitted<i32>,
},
String {
behavior: StringBehavior,
},
}

impl ParserProperties {
fn id(&self) -> VarInt {
match self {
Self::Float { .. } => VarInt::new(1),
Self::Integer { .. } => VarInt::new(3),
Self::String { .. } => VarInt::new(5),
}
}

fn identifier(&self) -> Identifier {
match self {
ParserProperties::Float { .. } => Identifier::new("brigadier", "float"),
ParserProperties::Integer { .. } => Identifier::new("brigadier", "integer"),
ParserProperties::String { .. } => Identifier::new("brigadier", "string"),
}
}

Expand All @@ -214,6 +256,18 @@ impl ParserProperties {
max: Omitted::Some(max),
}
}

fn integer(min: i32, max: i32) -> Self {
Self::Integer {
flags: 0x01 | 0x02,
min: Omitted::Some(min),
max: Omitted::Some(max),
}
}

fn string(behavior: StringBehavior) -> Self {
Self::String { behavior }
}
}

impl EncodePacket for ParserProperties {
Expand All @@ -234,6 +288,14 @@ impl EncodePacket for ParserProperties {
min.encode(writer, protocol_version)?;
max.encode(writer, protocol_version)?;
}
ParserProperties::Integer { flags, min, max } => {
flags.encode(writer, protocol_version)?;
min.encode(writer, protocol_version)?;
max.encode(writer, protocol_version)?;
}
ParserProperties::String { behavior } => {
(*behavior as i8).encode(writer, protocol_version)?;
}
}
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions crates/minecraft_packets/src/play/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod set_titles_animation;
pub mod synchronize_player_position_packet;
pub mod system_chat_message_packet;
pub mod tab_list_packet;
pub mod transfer_packet;
pub mod update_time_packet;

pub use data::chunk_context::{VoidChunkContext, WorldContext};
17 changes: 17 additions & 0 deletions crates/minecraft_packets/src/play/transfer_packet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
use minecraft_protocol::prelude::*;

#[derive(PacketOut)]
pub struct TransferPacket {
// TODO should this be named PlayTransferPacket since there are also configuration phase transfers?
pub host: String,
pub port: VarInt,
}

impl TransferPacket {
pub fn new(host: &str, port: &VarInt) -> Self {
Self {
host: host.to_owned(),
port: port.clone(),
}
}
}
6 changes: 6 additions & 0 deletions data/generated/V1_20_5/reports/packets.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
"minecraft:registry_data": {
"protocol_id": 7
},
"minecraft:transfer": {
"protocol_id": 11
},
"minecraft:select_known_packs": {
"protocol_id": 14
},
Expand Down Expand Up @@ -127,6 +130,9 @@
},
"minecraft:player_abilities": {
"protocol_id": 56
},
"minecraft:transfer": {
"protocol_id": 115
}
},
"serverbound": {
Expand Down
6 changes: 1 addition & 5 deletions docs/about/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,7 @@ If you need authenticated players, you should handle authentication at the proxy
PicoLimbo cannot load existing worlds or generate terrain. Players connect to a void environment by default.

However, PicoLimbo includes experimental support for loading small structures using `.schem` files. See the [Experimental World Loading](/config/world.html) section for configuration details.

## Does PicoLimbo support transfer packets?

Transfer packet support is not currently implemented in PicoLimbo, but this is a planned feature for a future release.

s
## Does PicoLimbo support Bedrock players?

While it is not planned in the near future, I understand the need for such a feature. In the meantime, you can probably install Geyser as a plugin on a Velocity proxy.
15 changes: 15 additions & 0 deletions docs/config/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ fly_speed = "flyspeed"
```
:::

## Transfer Command

The `/transfer` command allows players to transfer to another server by specifying its `hostname` and optionally a `port`. If a port is not specified the Minecraft default of 25565 is used.

Note that the destination server must have [accepts-transfers](https://minecraft.wiki/w/Server.properties#Keys) set to true in its server.properties.

:::code-group
```toml [server.toml] {2}
[commands]
transfer = "transfer"
```
:::

## Disabling Commands

Any command can be disabled by setting its value to an empty string `""`. This prevents players from using that command entirely.
Expand All @@ -45,6 +58,7 @@ Any command can be disabled by setting its value to an empty string `""`. This p
spawn = ""
fly = "fly"
fly_speed = ""
transfer = ""
```
:::

Expand All @@ -58,5 +72,6 @@ You can rename any command to a custom alias by changing its value. For example,
spawn = "home"
fly = "soar"
fly_speed = "speed"
transfer = "server"
```
:::
10 changes: 10 additions & 0 deletions docs/config/server-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,13 @@ fetch_player_skins = true

> [!WARNING]
> If you expect a large amount of player to connect to your limbo server instance, your server's IP may get black listed from Mojang API.

## Accept Transfers

Whether players who have been transferred from another server via a [transfer packet](https://minecraft.wiki/w/Commands/transfer) should be accepted.

:::code-group
```toml [server.toml]
accept_transfers = false
```
:::
2 changes: 2 additions & 0 deletions pico_limbo/src/configuration/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub struct CommandsConfig {
pub spawn: String,
pub fly: String,
pub fly_speed: String,
pub transfer: String,
}

impl Default for CommandsConfig {
Expand All @@ -14,6 +15,7 @@ impl Default for CommandsConfig {
spawn: "spawn".to_string(),
fly: "fly".to_string(),
fly_speed: "flyspeed".to_string(),
transfer: "transfer".to_string(),
}
}
}
3 changes: 3 additions & 0 deletions pico_limbo/src/configuration/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ pub struct Config {

pub allow_flight: bool,

pub accept_transfers: bool,

pub boss_bar: BossBarConfig,

pub title: TitleConfig,
Expand All @@ -92,6 +94,7 @@ impl Default for Config {
title: TitleConfig::default(),
allow_unsupported_versions: false,
allow_flight: false,
accept_transfers: false,
commands: CommandsConfig::default(),
}
}
Expand Down
23 changes: 20 additions & 3 deletions pico_limbo/src/handlers/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use minecraft_packets::login::Property;
use minecraft_packets::play::boss_bar_packet::BossBarPacket;
use minecraft_packets::play::client_bound_player_abilities_packet::ClientBoundPlayerAbilitiesPacket;
use minecraft_packets::play::client_bound_plugin_message_packet::PlayClientBoundPluginMessagePacket;
use minecraft_packets::play::commands_packet::{Command, CommandArgument, CommandsPacket};
use minecraft_packets::play::commands_packet::{
Command, CommandArgument, CommandsPacket, StringBehavior,
};
use minecraft_packets::play::game_event_packet::GameEventPacket;
use minecraft_packets::play::legacy_chat_message_packet::LegacyChatMessagePacket;
use minecraft_packets::play::legacy_set_title_packet::LegacySetTitlePacket;
Expand Down Expand Up @@ -175,7 +177,7 @@ pub fn send_play_packets(
client_state.set_feet_position(y);

if protocol_version.is_after_inclusive(ProtocolVersion::V1_13) {
send_commands_packet(batch, server_state);
send_commands_packet(batch, protocol_version, server_state);
}

// The brand is not visible for clients prior to 1.13, no need to send it
Expand Down Expand Up @@ -384,7 +386,11 @@ fn send_skin_packets(
}
}

fn send_commands_packet(batch: &mut Batch<PacketRegistry>, server_state: &ServerState) {
fn send_commands_packet(
batch: &mut Batch<PacketRegistry>,
protocol_version: ProtocolVersion,
server_state: &ServerState,
) {
let mut commands = vec![];
if let ServerCommand::Enabled { alias } = server_state.server_commands().spawn() {
commands.push(Command::no_arguments(alias));
Expand All @@ -398,6 +404,17 @@ fn send_commands_packet(batch: &mut Batch<PacketRegistry>, server_state: &Server
vec![CommandArgument::float("speed", 0.0, 1.0)],
));
}
if protocol_version.is_after_inclusive(ProtocolVersion::V1_20_5)
&& let ServerCommand::Enabled { alias } = server_state.server_commands().transfer()
{
commands.push(Command::new(
alias,
vec![
CommandArgument::string("hostname", StringBehavior::SingleWord),
CommandArgument::integer("port", 0, 65535),
],
));
}
let packet = CommandsPacket::new(commands);
batch.queue(|| PacketRegistry::Commands(packet));
}
Expand Down
3 changes: 2 additions & 1 deletion pico_limbo/src/handlers/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ impl PacketHandler for HandshakePacket {
Ok(batch)
}
State::Transfer => {
if server_state.accepts_transfers() {
if server_state.accept_transfers() {
client_state.set_state(State::Login);
begin_login(client_state, server_state, &self.hostname)?;
Ok(batch)
} else {
Expand Down
41 changes: 40 additions & 1 deletion pico_limbo/src/handlers/play/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ use crate::server_state::{ServerCommand, ServerCommands, ServerState};
use minecraft_packets::play::chat_command_packet::ChatCommandPacket;
use minecraft_packets::play::chat_message_packet::ChatMessagePacket;
use minecraft_packets::play::client_bound_player_abilities_packet::ClientBoundPlayerAbilitiesPacket;
use minecraft_packets::play::transfer_packet::TransferPacket;
use minecraft_protocol::prelude::{ProtocolVersion, VarInt};
use thiserror::Error;
use tracing::info;
use tracing::{info, warn};

impl PacketHandler for ChatCommandPacket {
fn handle(
Expand Down Expand Up @@ -76,6 +78,30 @@ fn run_command(
batch.queue(|| PacketRegistry::ClientBoundPlayerAbilities(packet));
client_state.set_flying_speed(speed);
}
Command::Transfer(host, port) => {
if client_state
.protocol_version()
.is_after_inclusive(ProtocolVersion::V1_20_5)
{
info!(
"Transferring {} to {}:{}",
client_state.get_username(),
host,
port
);
let packet = TransferPacket {
host,
port: VarInt::from(port),
};
batch.queue(|| PacketRegistry::Transfer(packet));
} else {
warn!(
"{} tried to transfer servers on unsupported version {}",
client_state.get_username(),
client_state.protocol_version().humanize()
)
}
}
}
}
}
Expand All @@ -88,12 +114,17 @@ pub enum ParseCommandError {
Unknown,
#[error("invalid speed value")]
InvalidSpeed(#[from] std::num::ParseFloatError),
#[error("invalid hostname")]
InvalidHost,
#[error("invalid port")]
InvalidPort(#[from] std::num::ParseIntError),
}

enum Command {
Spawn,
Fly,
FlySpeed(f32),
Transfer(String, i32),
}

impl Command {
Expand All @@ -108,6 +139,14 @@ impl Command {
let speed_str = parts.next().unwrap_or("0.05");
let speed = speed_str.parse::<f32>()?.clamp(0.0, 1.0);
Ok(Self::FlySpeed(speed))
} else if Self::is_command(server_commands.transfer(), cmd) {
let host = parts
.next()
.ok_or(ParseCommandError::InvalidHost)?
.to_string();
let port_str = parts.next().unwrap_or("25565");
let port = port_str.parse::<i32>()?;
Ok(Self::Transfer(host, port))
} else {
Err(ParseCommandError::Unknown)
}
Expand Down
Loading