Merge remote-tracking branch 'upstream/master'

This commit is contained in:
lukas0008 2024-08-12 20:30:52 +02:00
commit 5d6c38d1b2
60 changed files with 1371 additions and 344 deletions

34
Cargo.lock generated
View File

@ -772,6 +772,16 @@ version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "md-5"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
dependencies = [
"cfg-if",
"digest 0.10.7",
]
[[package]]
name = "memchr"
version = "2.7.4"
@ -1098,6 +1108,7 @@ name = "pumpkin"
version = "0.1.0-dev"
dependencies = [
"base64",
"bytes",
"crossbeam-channel",
"digest 0.11.0-pre.9",
"image",
@ -1107,8 +1118,10 @@ dependencies = [
"num-derive",
"num-traits",
"pumpkin-entity",
"pumpkin-inventory",
"pumpkin-protocol",
"pumpkin-registry",
"pumpkin-text",
"pumpkin-world",
"rand",
"reqwest",
@ -1132,6 +1145,14 @@ dependencies = [
"num-traits",
]
[[package]]
name = "pumpkin-inventory"
version = "0.1.0"
dependencies = [
"num-derive",
"num-traits",
]
[[package]]
name = "pumpkin-macros"
version = "0.1.0"
@ -1148,13 +1169,13 @@ dependencies = [
"aes",
"bytes",
"cfb8",
"fastnbt",
"flate2",
"itertools",
"log",
"num-derive",
"num-traits",
"pumpkin-macros",
"pumpkin-text",
"pumpkin-world",
"serde",
"serde_json",
@ -1170,9 +1191,19 @@ dependencies = [
"fastnbt",
"fastsnbt",
"pumpkin-protocol",
"pumpkin-text",
"serde",
]
[[package]]
name = "pumpkin-text"
version = "0.1.0"
dependencies = [
"fastnbt",
"serde",
"uuid",
]
[[package]]
name = "pumpkin-world"
version = "0.1.0"
@ -1894,6 +1925,7 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
dependencies = [
"md-5",
"serde",
]

View File

@ -1,13 +1,15 @@
[workspace]
resolver = "2"
members = [ "pumpkin-entity", "pumpkin-macros/", "pumpkin-protocol/", "pumpkin-registry/", "pumpkin-world", "pumpkin/" ]
members = [ "pumpkin-entity", "pumpkin-inventory", "pumpkin-macros/", "pumpkin-protocol/", "pumpkin-registry/", "pumpkin-world", "pumpkin/" ]
[workspace.package]
version = "0.1.0"
edition = "2021"
[profile.release]
strip = true
lto = true
codegen-units = 1
[workspace.dependencies]
tokio = { version = "1.39.2", features = ["net", "macros", "rt-multi-thread", "fs", "io-util"] }

View File

@ -4,7 +4,7 @@
![CI](https://github.com/Snowiiii/Pumpkin/actions/workflows/rust.yml/badge.svg)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![Current version)](https://img.shields.io/badge/current_version-1.21-blue)
![Current version)](https://img.shields.io/badge/current_version-1.21.1-blue)
</div>
@ -27,13 +27,10 @@ Pumpkin is currently under heavy development.
### Features (WIP)
- [x] Configuration (toml)
- [x] Server Status/Ping
- [x] Custom maximum player amout
- [x] Custom maximum player amount
- [x] Custom Icon
- [x] Custom Status (MOTD)
- Login
- [x] Authentication
- [x] Encryption
- [x] Packet Compression
- [x] Login
- Player Configuration
- [x] Registries (biome types, paintings, dimensions)
- [x] Server Brand
@ -42,6 +39,7 @@ Pumpkin is currently under heavy development.
- [ ] Cookies
- World
- [x] World Joining
- [x] Player Tab-list
- [x] World Loading
- [x] Entity Spawning
- [ ] Chunk Loading
@ -57,6 +55,8 @@ Pumpkin is currently under heavy development.
- [ ] Player Inventory
- [ ] Player Attack
- Server
- [x] RCON
- [x] Inventories
- [x] Chat
- [x] Commands

View File

@ -0,0 +1,8 @@
[package]
name = "pumpkin-inventory"
version.workspace = true
edition.workspace = true
[dependencies]
num-traits = "0.2"
num-derive = "0.4"

View File

@ -0,0 +1,41 @@
use num_derive::ToPrimitive;
/// https://wiki.vg/Inventory
#[derive(Debug, ToPrimitive)]
pub enum WindowType {
// not used
Generic9x1,
// not used
Generic9x2,
// General-purpose 3-row inventory. Used by Chest, minecart with chest, ender chest, and barrel
Generic9x3,
// not used
Generic9x4,
// not used
Generic9x5,
// Used by large chests
Generic9x6,
// General-purpose 3-by-3 square inventory, used by Dispenser and Dropper
Generic3x3,
// General-purpose 3-by-3 square inventory, used by the Crafter
Craft3x3,
Anvil,
Beacon,
BlastFurance,
BrewingStand,
CraftingTable,
EnchantmentTable,
Furnace,
Grindstone,
// Hopper or minecart with hopper
Hopper,
Lectern,
Loom,
// Villager, Wandering Trader
Merchant,
ShulkerBox,
SmithingTable,
Smoker,
CartographyTable,
Stonecutter,
}

View File

@ -4,9 +4,9 @@ version.workspace = true
edition.workspace = true
[dependencies]
pumpkin-macros = { path = "../pumpkin-macros" }
pumpkin-world = { path = "../pumpkin-world" }
pumpkin-text = { path = "../pumpkin-text" }
bytes = "1.7"
@ -23,11 +23,9 @@ log = "0.4"
num-traits = "0.2"
num-derive = "0.4"
# for text component
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }
# encryption
aes = "0.8.4"
cfb8 = "0.8.1"
take_mut = "0.2.2"
itertools = "0.13.0"
fastnbt = "2.5.0"

View File

@ -0,0 +1,31 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
#[derive(Serialize)]
#[packet(0x09)]
pub struct CConfigAddResourcePack {
uuid: uuid::Uuid,
url: String,
hash: String,
forced: bool,
prompt_message: Option<TextComponent>,
}
impl CConfigAddResourcePack {
pub fn new(
uuid: uuid::Uuid,
url: String,
hash: String,
forced: bool,
prompt_message: Option<TextComponent>,
) -> Self {
Self {
uuid,
url,
hash,
forced,
prompt_message,
}
}
}

View File

@ -1,7 +1,9 @@
use pumpkin_macros::packet;
use serde::Serialize;
use crate::{bytebuf::ByteBuffer, ClientPacket};
#[derive(Serialize)]
#[packet(0x01)]
pub struct CPluginMessage<'a> {
channel: &'a str,
@ -13,10 +15,3 @@ impl<'a> CPluginMessage<'a> {
Self { channel, data }
}
}
impl<'a> ClientPacket for CPluginMessage<'a> {
fn write(&self, bytebuf: &mut ByteBuffer) {
bytebuf.put_string(self.channel);
bytebuf.put_slice(self.data);
}
}

View File

@ -1,3 +1,4 @@
mod c_add_resource_pack;
mod c_config_disconnect;
mod c_cookie_request;
mod c_finish_config;
@ -5,6 +6,7 @@ mod c_known_packs;
mod c_plugin_message;
mod c_registry_data;
pub use c_add_resource_pack::*;
pub use c_config_disconnect::*;
pub use c_cookie_request::*;
pub use c_finish_config::*;

View File

@ -1,31 +1,31 @@
use pumpkin_macros::packet;
use serde::Serialize;
use crate::{RawBytes, VarInt};
use crate::VarInt;
#[derive(Serialize)]
#[packet(0x01)]
pub struct CEncryptionRequest<'a> {
server_id: &'a str, // 20
public_key_length: VarInt,
public_key: RawBytes<'a>,
public_key: &'a [u8],
verify_token_length: VarInt,
verify_token: RawBytes<'a>,
verify_token: &'a [u8],
should_authenticate: bool,
}
impl<'a> CEncryptionRequest<'a> {
pub fn new(
server_id: &'a str,
public_key: RawBytes<'a>,
verify_token: RawBytes<'a>,
public_key: &'a [u8],
verify_token: &'a [u8],
should_authenticate: bool,
) -> Self {
Self {
server_id,
public_key_length: public_key.0.len().into(),
public_key_length: public_key.len().into(),
public_key,
verify_token_length: verify_token.0.len().into(),
verify_token_length: verify_token.len().into(),
verify_token,
should_authenticate,
}

View File

@ -5,7 +5,7 @@ use crate::{bytebuf::ByteBuffer, ClientPacket, Property};
#[packet(0x02)]
pub struct CLoginSuccess<'a> {
pub uuid: uuid::Uuid,
pub username: String, // 16
pub username: &'a str, // 16
pub properties: &'a [Property],
pub strict_error_handling: bool,
}
@ -13,7 +13,7 @@ pub struct CLoginSuccess<'a> {
impl<'a> CLoginSuccess<'a> {
pub fn new(
uuid: uuid::Uuid,
username: String,
username: &'a str,
properties: &'a [Property],
strict_error_handling: bool,
) -> Self {

View File

@ -0,0 +1,15 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
#[derive(Serialize)]
#[packet(0x4C)]
pub struct CActionBar {
action_bar: TextComponent,
}
impl CActionBar {
pub fn new(action_bar: TextComponent) -> Self {
Self { action_bar }
}
}

View File

@ -1,6 +1,7 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use crate::{text::TextComponent, ClientPacket, VarInt};
use crate::{ClientPacket, VarInt};
#[derive(Clone)]
#[packet(0x1E)]

View File

@ -4,7 +4,7 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x03)]
pub struct CEntityAnimation {
entity_id: VarInt,

View File

@ -3,23 +3,25 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x58)]
pub struct CSetEntityMetadata {
entity_id: VarInt,
metadata: Vec<Metadata>,
metadata: Metadata,
end: u8,
}
impl CSetEntityMetadata {
pub fn new(entity_id: VarInt, metadata: Vec<Metadata>) -> Self {
pub fn new(entity_id: VarInt, metadata: Metadata) -> Self {
Self {
entity_id,
metadata,
end: 255,
}
}
}
#[derive(Serialize, Clone)]
#[derive(Serialize)]
pub struct Metadata {
index: u8,
typ: VarInt,

View File

@ -3,7 +3,7 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x48)]
pub struct CHeadRot {
entity_id: VarInt,

View File

@ -5,11 +5,11 @@ use crate::VarInt;
#[derive(Serialize)]
#[packet(0x2B)]
pub struct CLogin {
pub struct CLogin<'a> {
entity_id: i32,
is_hardcore: bool,
dimension_count: VarInt,
dimension_names: Vec<String>,
dimension_names: &'a [&'a str],
max_players: VarInt,
view_distance: VarInt,
simulated_distance: VarInt,
@ -17,7 +17,7 @@ pub struct CLogin {
enabled_respawn_screen: bool,
limited_crafting: bool,
dimension_type: VarInt,
dimension_name: String,
dimension_name: &'a str,
hashed_seed: i64,
game_mode: u8,
previous_gamemode: i8,
@ -28,12 +28,12 @@ pub struct CLogin {
enforce_secure_chat: bool,
}
impl CLogin {
impl<'a> CLogin<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
entity_id: i32,
is_hardcore: bool,
dimension_names: Vec<String>,
dimension_names: &'a [&'a str],
max_players: VarInt,
view_distance: VarInt,
simulated_distance: VarInt,
@ -41,7 +41,7 @@ impl CLogin {
enabled_respawn_screen: bool,
limited_crafting: bool,
dimension_type: VarInt,
dimension_name: String,
dimension_name: &'a str,
hashed_seed: i64,
game_mode: u8,
previous_gamemode: i8,

View File

@ -0,0 +1,23 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
use crate::VarInt;
#[derive(Serialize)]
#[packet(0x33)]
pub struct COpenScreen {
window_id: VarInt,
window_type: VarInt,
window_title: TextComponent,
}
impl COpenScreen {
pub fn new(window_id: VarInt, window_type: VarInt, window_title: TextComponent) -> Self {
Self {
window_id,
window_type,
window_title,
}
}
}

View File

@ -1,8 +1,7 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
use crate::text::TextComponent;
#[derive(Serialize)]
#[packet(0x1D)]
pub struct CPlayDisconnect {

View File

@ -1,22 +1,26 @@
use num_derive::{FromPrimitive, ToPrimitive};
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
use crate::{text::TextComponent, VarInt};
use crate::{uuid::UUID, BitSet, VarInt};
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x39)]
pub struct CPlayerChatMessage<'a> {
sender: uuid::Uuid,
sender: UUID,
index: VarInt,
message_signature: Option<&'a [u8]>,
message: String,
timestamp: i64,
salt: i64,
previous_messages_count: VarInt,
previous_messages: &'a [PreviousMessage<'a>], // max 20
unsigned_content: Option<TextComponent>,
/// See `FilterType`
filter_type: VarInt,
// TODO: THIS IS A HACK, We currently don't support writing or reading bitsets
filter_type_bits: bool,
chat_type: VarInt,
sender_name: TextComponent,
target_name: Option<TextComponent>,
@ -25,7 +29,7 @@ pub struct CPlayerChatMessage<'a> {
impl<'a> CPlayerChatMessage<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
sender: uuid::Uuid,
sender: UUID,
index: VarInt,
message_signature: Option<&'a [u8]>,
message: String,
@ -45,9 +49,11 @@ impl<'a> CPlayerChatMessage<'a> {
message,
timestamp,
salt,
previous_messages_count: previous_messages.len().into(),
previous_messages,
unsigned_content,
filter_type,
filter_type_bits: false,
chat_type,
sender_name,
target_name,

View File

@ -0,0 +1,20 @@
use pumpkin_macros::packet;
use serde::Serialize;
use crate::{uuid::UUID, VarInt};
#[derive(Serialize)]
#[packet(0x3D)]
pub struct CRemovePlayerInfo<'a> {
players_count: VarInt,
players: &'a [UUID],
}
impl<'a> CRemovePlayerInfo<'a> {
pub fn new(players_count: VarInt, players: &'a [UUID]) -> Self {
Self {
players_count,
players,
}
}
}

View File

@ -3,14 +3,18 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x42)]
pub struct CRemoveEntities {
entitiy_ids: Vec<VarInt>,
pub struct CRemoveEntities<'a> {
count: VarInt,
entitiy_ids: &'a [VarInt],
}
impl CRemoveEntities {
pub fn new(entitiy_ids: Vec<VarInt>) -> Self {
Self { entitiy_ids }
impl<'a> CRemoveEntities<'a> {
pub fn new(entitiy_ids: &'a [VarInt]) -> Self {
Self {
count: VarInt(entitiy_ids.len() as i32),
entitiy_ids,
}
}
}

View File

@ -0,0 +1,15 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
#[derive(Serialize)]
#[packet(0x65)]
pub struct CTitleText {
title: TextComponent,
}
impl CTitleText {
pub fn new(title: TextComponent) -> Self {
Self { title }
}
}

View File

@ -1,12 +1,13 @@
use pumpkin_macros::packet;
use serde::Serialize;
use crate::{ClientPacket, VarInt};
use crate::{uuid::UUID, VarInt};
#[derive(Clone)]
#[derive(Serialize)]
#[packet(0x01)]
pub struct CSpawnEntity {
entity_id: VarInt,
entity_uuid: uuid::Uuid,
entity_uuid: UUID,
typ: VarInt,
x: f64,
y: f64,
@ -24,18 +25,18 @@ impl CSpawnEntity {
#[allow(clippy::too_many_arguments)]
pub fn new(
entity_id: VarInt,
entity_uuid: uuid::Uuid,
entity_uuid: UUID,
typ: VarInt,
x: f64,
y: f64,
z: f64,
pitch: u8, // angle
yaw: u8, // angle
head_yaw: u8, // angle
pitch: f32, // angle
yaw: f32, // angle
head_yaw: f32, // angle
data: VarInt,
velocity_x: i16,
velocity_y: i16,
velocity_z: i16,
velocity_x: f32,
velocity_y: f32,
velocity_z: f32,
) -> Self {
Self {
entity_id,
@ -44,31 +45,13 @@ impl CSpawnEntity {
x,
y,
z,
pitch,
yaw,
head_yaw,
pitch: (pitch * 256.0 / 360.0).floor() as u8,
yaw: (yaw * 256.0 / 360.0).floor() as u8,
head_yaw: (head_yaw * 256.0 / 360.0).floor() as u8,
data,
velocity_x,
velocity_y,
velocity_z,
velocity_x: (velocity_x.clamp(-3.9, 3.9) * 8000.0) as i16,
velocity_y: (velocity_y.clamp(-3.9, 3.9) * 8000.0) as i16,
velocity_z: (velocity_z.clamp(-3.9, 3.9) * 8000.0) as i16,
}
}
}
impl ClientPacket for CSpawnEntity {
fn write(&self, bytebuf: &mut crate::bytebuf::ByteBuffer) {
bytebuf.put_var_int(&self.entity_id);
bytebuf.put_uuid(self.entity_uuid);
bytebuf.put_var_int(&self.typ);
bytebuf.put_f64(self.x);
bytebuf.put_f64(self.y);
bytebuf.put_f64(self.z);
bytebuf.put_u8(self.pitch);
bytebuf.put_u8(self.yaw);
bytebuf.put_u8(self.head_yaw);
bytebuf.put_var_int(&self.data);
bytebuf.put_i16(self.velocity_x);
bytebuf.put_i16(self.velocity_y);
bytebuf.put_i16(self.velocity_z);
}
}

View File

@ -0,0 +1,15 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
#[derive(Serialize)]
#[packet(0x63)]
pub struct CSubtitle {
subtitle: TextComponent,
}
impl CSubtitle {
pub fn new(subtitle: TextComponent) -> Self {
Self { subtitle }
}
}

View File

@ -1,9 +1,8 @@
use pumpkin_macros::packet;
use pumpkin_text::TextComponent;
use serde::Serialize;
use crate::text::TextComponent;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x6C)]
pub struct CSystemChatMessge {
content: TextComponent,

View File

@ -3,7 +3,7 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x2F)]
pub struct CUpdateEntityPosRot {
entity_id: VarInt,

View File

@ -3,7 +3,7 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x2E)]
pub struct CUpdateEntityPos {
entity_id: VarInt,

View File

@ -3,7 +3,7 @@ use serde::Serialize;
use crate::VarInt;
#[derive(Serialize, Clone)]
#[derive(Serialize)]
#[packet(0x30)]
pub struct CUpdateEntityRot {
entity_id: VarInt,

View File

@ -1,4 +1,5 @@
mod c_center_chunk;
mod c_actionbar;
mod c_change_difficulty;
mod c_chunk_data;
mod c_chunk_data_update_light;
@ -8,13 +9,17 @@ mod c_entity_metadata;
mod c_game_event;
mod c_head_rot;
mod c_login;
mod c_open_screen;
mod c_play_disconnect;
mod c_player_abilities;
mod c_player_chat_message;
mod c_player_info_update;
mod c_player_remove;
mod c_remove_entities;
mod c_set_held_item;
mod c_set_title;
mod c_spawn_player;
mod c_subtitle;
mod c_sync_player_position;
mod c_system_chat_message;
mod c_update_entitiy_pos_rot;
@ -23,6 +28,7 @@ mod c_update_entity_rot;
mod player_action;
pub use c_center_chunk::*;
pub use c_actionbar::*;
pub use c_change_difficulty::*;
pub use c_chunk_data::*;
pub use c_chunk_data_update_light::*;
@ -32,13 +38,17 @@ pub use c_entity_metadata::*;
pub use c_game_event::*;
pub use c_head_rot::*;
pub use c_login::*;
pub use c_open_screen::*;
pub use c_play_disconnect::*;
pub use c_player_abilities::*;
pub use c_player_chat_message::*;
pub use c_player_info_update::*;
pub use c_player_remove::*;
pub use c_remove_entities::*;
pub use c_set_held_item::*;
pub use c_set_title::*;
pub use c_spawn_player::*;
pub use c_subtitle::*;
pub use c_sync_player_position::*;
pub use c_system_chat_message::*;
pub use c_update_entitiy_pos_rot::*;

View File

@ -2,13 +2,13 @@ use std::io::{self, Write};
use bytebuf::{packet_id::Packet, ByteBuffer, DeserializerError};
use bytes::Buf;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize, Serializer};
use thiserror::Error;
pub mod bytebuf;
pub mod client;
pub mod server;
pub mod text;
pub mod uuid;
pub mod packet_decoder;
pub mod packet_encoder;
@ -91,16 +91,6 @@ impl VarInt {
}
}
pub struct RawBytes<'a>(pub &'a [u8]);
impl<'a> Serialize for RawBytes<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(self.0)
}
}
impl From<i32> for VarInt {
fn from(value: i32) -> Self {
VarInt(value)

View File

@ -22,7 +22,7 @@ pub struct PacketEncoder {
}
impl PacketEncoder {
pub fn append_packet<P: ClientPacket>(&mut self, packet: P) -> Result<(), PacketError> {
pub fn append_packet<P: ClientPacket>(&mut self, packet: &P) -> Result<(), PacketError> {
let start_len = self.buf.len();
let mut writer = (&mut self.buf).writer();

View File

@ -1,79 +0,0 @@
use core::str;
use fastnbt::SerOpts;
use serde::Deserialize;
// Fepresents a Text component
// Reference: https://wiki.vg/Text_formatting#Text_components
#[derive(Clone, PartialEq, Default, Debug, Deserialize)]
pub struct TextComponent {
pub text: String,
}
impl serde::Serialize for TextComponent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(self.encode().as_slice())
}
}
impl TextComponent {
pub fn encode(&self) -> Vec<u8> {
#[derive(serde::Serialize)]
struct TempStruct<'a> {
text: &'a String,
}
fastnbt::to_bytes_with_opts(&TempStruct { text: &self.text }, SerOpts::network_nbt())
.unwrap()
}
}
impl From<String> for TextComponent {
fn from(value: String) -> Self {
Self { text: value }
}
}
impl From<&str> for TextComponent {
fn from(value: &str) -> Self {
Self {
text: value.to_string(),
}
}
}
/// Text color
#[derive(Default, Debug, Clone, Copy)]
pub enum Color {
/// The default color for the text will be used, which varies by context
/// (in some cases, it's white; in others, it's black; in still others, it
/// is a shade of gray that isn't normally used on text).
#[default]
Reset,
/// One of the 16 named Minecraft colors
Named(NamedColor),
}
/// Named Minecraft color
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum NamedColor {
Black = 0,
DarkBlue,
DarkGreen,
DarkCyan,
DarkRed,
Purple,
Gold,
Gray,
DarkGray,
Blue,
BrightGreen,
Cyan,
Red,
Pink,
Yellow,
White,
}

View File

@ -0,0 +1,13 @@
use serde::Serialize;
#[derive(Clone)]
pub struct UUID(pub uuid::Uuid);
impl Serialize for UUID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(self.0.as_bytes())
}
}

View File

@ -5,6 +5,8 @@ edition.workspace = true
[dependencies]
pumpkin-protocol = { path = "../pumpkin-protocol"}
pumpkin-text = { path = "../pumpkin-text"}
# nbt
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }
fastsnbt = "0.2"

View File

@ -1 +1,33 @@
use pumpkin_text::style::Style;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct ChatType {
chat: Decoration,
narration: Decoration,
}
#[derive(Debug, Clone, Serialize)]
pub struct Decoration {
translation_key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
style: Option<Style>,
parameters: Vec<String>,
}
impl Default for ChatType {
fn default() -> Self {
Self {
chat: Decoration {
style: None,
parameters: vec!["sender".into(), "content".into()],
translation_key: "chat.type.text".into(),
},
narration: Decoration {
style: None,
parameters: vec!["sender".into(), "content".into()],
translation_key: "chat.type.text.narrate".into(),
},
}
}
}

View File

@ -1,4 +1,5 @@
use biomes::Biome;
use chat_type::ChatType;
use dimensions::Dimension;
use fastnbt::SerOpts;
use paint::Painting;
@ -50,6 +51,16 @@ impl Registry {
.unwrap(),
}],
};
let chat_types = Registry {
registry_id: "minecraft:chat_type".to_string(),
registry_entries: vec![RegistryEntry {
entry_id: "minecraft:chat",
data: fastnbt::to_bytes_with_opts(&ChatType::default(), SerOpts::network_nbt())
.unwrap(),
}],
};
let damage_types = Registry {
registry_id: "minecraft:damage_type".to_string(),
registry_entries: damage_type::entires(),
@ -62,6 +73,13 @@ impl Registry {
.unwrap(),
}],
};
vec![dimensions, damage_types, biomes, wolf_variants, paintings]
vec![
dimensions,
damage_types,
biomes,
wolf_variants,
paintings,
chat_types,
]
}
}

9
pumpkin-text/Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "pumpkin-text"
version.workspace = true
edition.workspace = true
[dependencies]
serde = { version = "1.0", features = ["derive"] }
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }
uuid = "1.10"

19
pumpkin-text/src/click.rs Normal file
View File

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
/// Action to take on click of the text.
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "action", content = "value", rename_all = "snake_case")]
pub enum ClickEvent {
/// Opens an URL
OpenUrl(String),
/// Works in signs, but only on the root text component
RunCommand(String),
/// Replaces the contents of the chat box with the text, not necessarily a
/// command.
SuggestCommand(String),
/// Only usable within written books. Changes the page of the book. Indexing
/// starts at 1.
ChangePage(i32),
/// Copies the given text to system clipboard
CopyToClipboard(String),
}

36
pumpkin-text/src/color.rs Normal file
View File

@ -0,0 +1,36 @@
use serde::{Deserialize, Serialize};
/// Text color
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Color {
/// The default color for the text will be used, which varies by context
/// (in some cases, it's white; in others, it's black; in still others, it
/// is a shade of gray that isn't normally used on text).
#[default]
Reset,
/// One of the 16 named Minecraft colors
Named(NamedColor),
}
/// Named Minecraft color
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NamedColor {
Black = 0,
DarkBlue,
DarkGreen,
DarkAqua,
DarkRed,
DarkPurple,
Gold,
Gray,
DarkGray,
Blue,
Green,
Aqua,
Red,
LightPurple,
Yellow,
White,
}

32
pumpkin-text/src/hover.rs Normal file
View File

@ -0,0 +1,32 @@
use serde::{Deserialize, Serialize};
use crate::Text;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "action", content = "contents", rename_all = "snake_case")]
#[allow(clippy::enum_variant_names)]
pub enum HoverEvent {
/// Displays a tooltip with the given text.
ShowText(Text),
/// Shows an item.
ShowItem {
/// Resource identifier of the item
id: String,
/// Number of the items in the stack
count: Option<i32>,
/// NBT information about the item (sNBT format)
tag: String,
},
/// Shows an entity.
ShowEntity {
/// The entity's UUID
id: uuid::Uuid,
/// Resource identifier of the entity
#[serde(rename = "type")]
#[serde(default, skip_serializing_if = "Option::is_none")]
kind: Option<String>,
/// Optional custom name for the entity
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<Text>,
},
}

167
pumpkin-text/src/lib.rs Normal file
View File

@ -0,0 +1,167 @@
use core::str;
use click::ClickEvent;
use color::Color;
use fastnbt::SerOpts;
use hover::HoverEvent;
use serde::{Deserialize, Serialize};
use style::Style;
pub mod click;
pub mod color;
pub mod hover;
pub mod style;
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Text(pub Box<TextComponent>);
// Fepresents a Text component
// Reference: https://wiki.vg/Text_formatting#Text_components
#[derive(Clone, Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextComponent {
/// The actual text
#[serde(flatten)]
pub content: TextContent,
/// Style of the text. Bold, Italic, unterline, Color...
/// Also has `ClickEvent
#[serde(flatten)]
pub style: Style,
}
impl serde::Serialize for TextComponent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_bytes(self.encode().as_slice())
}
}
impl TextComponent {
pub fn color(mut self, color: Color) -> Self {
self.style.color = Some(color);
self
}
pub fn color_named(mut self, color: color::NamedColor) -> Self {
self.style.color = Some(Color::Named(color));
self
}
/// Makes the text bold
pub fn bold(mut self) -> Self {
self.style.bold = Some(1);
self
}
/// Makes the text italic
pub fn italic(mut self) -> Self {
self.style.italic = Some(1);
self
}
/// Makes the text underlined
pub fn underlined(mut self) -> Self {
self.style.underlined = Some(1);
self
}
/// Makes the text strikethrough
pub fn strikethrough(mut self) -> Self {
self.style.strikethrough = Some(1);
self
}
/// Makes the text obfuscated
pub fn obfuscated(mut self) -> Self {
self.style.obfuscated = Some(1);
self
}
/// When the text is shift-clicked by a player, this string is inserted in their chat input. It does not overwrite any existing text the player was writing. This only works in chat messages
pub fn insertion(mut self, text: String) -> Self {
self.style.insertion = Some(text);
self
}
/// Allows for events to occur when the player clicks on text. Only work in chat.
pub fn click_event(mut self, event: ClickEvent) -> Self {
self.style.click_event = Some(event);
self
}
/// Allows for a tooltip to be displayed when the player hovers their mouse over text.
pub fn hover_event(mut self, event: HoverEvent) -> Self {
self.style.hover_event = Some(event);
self
}
pub fn encode(&self) -> Vec<u8> {
// TODO: Somehow fix this ugly mess
#[derive(serde::Serialize, Debug)]
#[serde(rename_all = "camelCase")]
struct TempStruct<'a> {
#[serde(flatten)]
text: &'a TextContent,
#[serde(flatten)]
pub style: &'a Style,
}
let astruct = TempStruct {
text: &self.content,
style: &self.style,
};
// dbg!(&serde_json::to_string(&astruct));
fastnbt::to_bytes_with_opts(&astruct, SerOpts::network_nbt()).unwrap()
}
}
impl From<String> for TextComponent {
fn from(value: String) -> Self {
Self {
content: TextContent::Text { text: value },
style: Style::default(),
}
}
}
impl From<&str> for TextComponent {
fn from(value: &str) -> Self {
Self {
content: TextContent::Text {
text: value.to_string(),
},
style: Style::default(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum TextContent {
/// Raw Text
Text { text: String },
/// Translated text
Translate {
translate: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
with: Vec<Text>,
},
/// Displays the name of one or more entities found by a selector.
EntityNames {
selector: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
separator: Option<Text>,
},
/// A keybind identifier
/// https://minecraft.fandom.com/wiki/Controls#Configurable_controls
Keybind { keybind: String },
}
impl Default for TextContent {
fn default() -> Self {
Self::Text { text: "".into() }
}
}

100
pumpkin-text/src/style.rs Normal file
View File

@ -0,0 +1,100 @@
use serde::{Deserialize, Serialize};
use crate::{
click::ClickEvent,
color::{self, Color},
hover::HoverEvent,
};
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct Style {
/// Changes the color to render the content
pub color: Option<Color>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bold: Option<u8>,
/// Whether to render the content in italic.
/// Keep in mind that booleans are representet as bytes in nbt
#[serde(default, skip_serializing_if = "Option::is_none")]
pub italic: Option<u8>,
/// Whether to render the content in underlined.
/// Keep in mind that booleans are representet as bytes in nbt
#[serde(default, skip_serializing_if = "Option::is_none")]
pub underlined: Option<u8>,
/// Whether to render the content in strikethrough.
/// Keep in mind that booleans are representet as bytes in nbt
#[serde(default, skip_serializing_if = "Option::is_none")]
pub strikethrough: Option<u8>,
/// Whether to render the content in obfuscated.
/// Keep in mind that booleans are representet as bytes in nbt
#[serde(default, skip_serializing_if = "Option::is_none")]
pub obfuscated: Option<u8>,
/// When the text is shift-clicked by a player, this string is inserted in their chat input. It does not overwrite any existing text the player was writing. This only works in chat messages
#[serde(default, skip_serializing_if = "Option::is_none")]
pub insertion: Option<String>,
/// Allows for events to occur when the player clicks on text. Only work in chat.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub click_event: Option<ClickEvent>,
/// Allows for a tooltip to be displayed when the player hovers their mouse over text.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hover_event: Option<HoverEvent>,
}
impl Style {
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
pub fn color_named(mut self, color: color::NamedColor) -> Self {
self.color = Some(Color::Named(color));
self
}
/// Makes the text bold
pub fn bold(mut self) -> Self {
self.bold = Some(1);
self
}
/// Makes the text italic
pub fn italic(mut self) -> Self {
self.italic = Some(1);
self
}
/// Makes the text underlined
pub fn underlined(mut self) -> Self {
self.underlined = Some(1);
self
}
/// Makes the text strikethrough
pub fn strikethrough(mut self) -> Self {
self.strikethrough = Some(1);
self
}
/// Makes the text obfuscated
pub fn obfuscated(mut self) -> Self {
self.obfuscated = Some(1);
self
}
/// When the text is shift-clicked by a player, this string is inserted in their chat input. It does not overwrite any existing text the player was writing. This only works in chat messages
pub fn insertion(mut self, text: String) -> Self {
self.insertion = Some(text);
self
}
/// Allows for events to occur when the player clicks on text. Only work in chat.
pub fn click_event(mut self, event: ClickEvent) -> Self {
self.click_event = Some(event);
self
}
/// Allows for a tooltip to be displayed when the player hovers their mouse over text.
pub fn hover_event(mut self, event: HoverEvent) -> Self {
self.hover_event = Some(event);
self
}
}

View File

@ -5,6 +5,8 @@ description = "Empowering everyone to host fast and efficient Minecraft servers.
edition = "2021"
[dependencies]
pumpkin-text = { path = "../pumpkin-text" }
pumpkin-inventory = { path = "../pumpkin-inventory"}
pumpkin-world = { path = "../pumpkin-world"}
pumpkin-entity = { path = "../pumpkin-entity"}
pumpkin-protocol = { path = "../pumpkin-protocol"}
@ -15,6 +17,8 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8.19"
bytes = "1.7"
rand = "0.8.5"
num-traits = "0.2"
@ -45,6 +49,6 @@ log = "0.4"
mio = { version = "1.0.1", features = ["os-poll", "net"]}
crossbeam-channel = "0.5.13"
uuid = { version = "1.10", features = ["serde"]}
uuid = { version = "1.10", features = ["serde", "v3"]}
tokio.workspace = true

View File

@ -52,14 +52,16 @@ pub async fn authenticate(
ip: &IpAddr,
server: &mut Server,
) -> Result<GameProfile, AuthError> {
assert!(server.advanced_config.authentication.use_authentication);
assert!(server.advanced_config.authentication.enabled);
assert!(server.auth_client.is_some());
let address = if server
.advanced_config
.authentication
.prevent_proxy_connections
{
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}")
let test = format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}");
dbg!(&test);
test
} else {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}")
};

View File

@ -1,7 +1,9 @@
use std::str::FromStr;
use num_traits::FromPrimitive;
use pumpkin_protocol::{
client::{
config::{CFinishConfig, CKnownPacks, CRegistryData},
config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData},
login::{CEncryptionRequest, CLoginSuccess, CSetCompression},
status::{CPingResponse, CStatusResponse},
},
@ -11,9 +13,9 @@ use pumpkin_protocol::{
login::{SEncryptionResponse, SLoginAcknowledged, SLoginPluginResponse, SLoginStart},
status::{SPingRequest, SStatusRequest},
},
ConnectionState, KnownPack, RawBytes,
ConnectionState, KnownPack,
};
use pumpkin_registry::Registry;
use pumpkin_text::TextComponent;
use rsa::Pkcs1v15Encrypt;
use sha1::{Digest, Sha1};
@ -38,12 +40,12 @@ impl Client {
}
pub fn handle_status_request(&mut self, server: &mut Server, _status_request: SStatusRequest) {
self.send_packet(CStatusResponse::new(&server.status_response_json));
self.send_packet(&CStatusResponse::new(&server.status_response_json));
}
pub fn handle_ping_request(&mut self, _server: &mut Server, ping_request: SPingRequest) {
dbg!("ping");
self.send_packet(CPingResponse::new(ping_request.payload));
self.send_packet(&CPingResponse::new(ping_request.payload));
self.close();
}
@ -64,11 +66,11 @@ impl Client {
let public_key_der = &server.public_key_der;
let packet = CEncryptionRequest::new(
"",
RawBytes(public_key_der),
RawBytes(&verify_token),
public_key_der,
&verify_token,
server.base_config.online_mode, // TODO
);
self.send_packet(packet);
self.send_packet(&packet);
}
pub async fn handle_encryption_response(
@ -131,6 +133,7 @@ impl Client {
}
}
for ele in self.gameprofile.as_ref().unwrap().properties.clone() {
// todo, use this
unpack_textures(ele, &server.advanced_config.authentication.textures);
}
@ -141,13 +144,13 @@ impl Client {
.packet_compression
.compression_threshold;
let level = server.advanced_config.packet_compression.compression_level;
self.send_packet(CSetCompression::new(threshold.into()));
self.send_packet(&CSetCompression::new(threshold.into()));
self.set_compression(Some((threshold, level)));
}
if let Some(profile) = self.gameprofile.as_ref().cloned() {
let packet = CLoginSuccess::new(profile.id, profile.name, &profile.properties, false);
self.send_packet(packet);
let packet = CLoginSuccess::new(profile.id, &profile.name, &profile.properties, false);
self.send_packet(&packet);
} else {
self.kick("game profile is none");
}
@ -167,8 +170,25 @@ impl Client {
) {
self.connection_state = ConnectionState::Config;
server.send_brand(self);
let resource_config = &server.advanced_config.resource_pack;
if resource_config.enabled {
let prompt_message = if resource_config.prompt_message.is_empty() {
None
} else {
Some(TextComponent::from(resource_config.prompt_message.clone()))
};
self.send_packet(&CConfigAddResourcePack::new(
uuid::Uuid::from_str(&resource_config.resource_pack_url).unwrap(),
resource_config.resource_pack_url.clone(),
resource_config.resource_pack_sha1.clone(),
resource_config.force,
prompt_message,
));
}
// known data packs
self.send_packet(CKnownPacks::new(&[KnownPack {
self.send_packet(&CKnownPacks::new(&[KnownPack {
namespace: "minecraft",
id: "core",
version: "1.21",
@ -180,6 +200,7 @@ impl Client {
_server: &mut Server,
client_information: SClientInformation,
) {
dbg!("got client settings");
self.config = Some(PlayerConfig {
locale: client_information.locale,
view_distance: client_information.view_distance,
@ -204,9 +225,9 @@ impl Client {
}
}
pub fn handle_known_packs(&mut self, _server: &mut Server, _config_acknowledged: SKnownPacks) {
for registry in Registry::get_static() {
self.send_packet(CRegistryData::new(
pub fn handle_known_packs(&mut self, server: &mut Server, _config_acknowledged: SKnownPacks) {
for registry in &server.cached_registry {
self.send_packet(&CRegistryData::new(
&registry.registry_id,
&registry.registry_entries,
));
@ -214,7 +235,7 @@ impl Client {
// We are done with configuring
dbg!("finish config");
self.send_packet(CFinishConfig::new());
self.send_packet(&CFinishConfig::new());
}
pub async fn handle_config_acknowledged(

View File

@ -32,9 +32,9 @@ use pumpkin_protocol::{
},
status::{SPingRequest, SStatusRequest},
},
text::TextComponent,
ClientPacket, ConnectionState, PacketError, RawPacket, ServerPacket,
};
use pumpkin_text::TextComponent;
use std::io::Read;
use thiserror::Error;
@ -122,7 +122,7 @@ impl Client {
}
/// Send a Clientbound Packet to the Client
pub fn send_packet<P: ClientPacket>(&mut self, packet: P) {
pub fn send_packet<P: ClientPacket>(&mut self, packet: &P) {
self.enc
.append_packet(packet)
.unwrap_or_else(|e| self.kick(&e.to_string()));
@ -132,7 +132,7 @@ impl Client {
.unwrap_or_else(|e| self.kick(&e.to_string()));
}
pub fn try_send_packet<P: ClientPacket>(&mut self, packet: P) -> Result<(), PacketError> {
pub fn try_send_packet<P: ClientPacket>(&mut self, packet: &P) -> Result<(), PacketError> {
self.enc.append_packet(packet)?;
self.connection
.write_all(&self.enc.take())
@ -155,11 +155,11 @@ impl Client {
entity.yaw = yaw;
entity.pitch = pitch;
player.awaiting_teleport = Some(id.into());
self.send_packet(CSyncPlayerPostion::new(x, y, z, yaw, pitch, 0, id.into()));
self.send_packet(&CSyncPlayerPostion::new(x, y, z, yaw, pitch, 0, id.into()));
}
pub fn set_gamemode(&mut self, gamemode: GameMode) {
self.send_packet(CGameEvent::new(3, gamemode.to_f32().unwrap()));
self.send_packet(&CGameEvent::new(3, gamemode.to_f32().unwrap()));
}
pub async fn process_packets(&mut self, server: &mut Server) {
@ -294,9 +294,7 @@ impl Client {
}
Ok(n) => {
bytes_read += n;
if bytes_read == received_data.len() {
received_data.resize(received_data.len() + 1024, 0);
}
received_data.extend(&vec![0; n]);
}
// Would block "errors" are the OS's way of saying that the
// connection is not actually ready to perform this I/O operation.
@ -325,7 +323,7 @@ impl Client {
}
pub fn send_system_message(&mut self, text: TextComponent) {
self.send_packet(CSystemChatMessge::new(text, false));
self.send_packet(&CSystemChatMessge::new(text, false));
}
/// Kicks the Client with a reason depending on the connection state
@ -333,19 +331,17 @@ impl Client {
dbg!(reason);
match self.connection_state {
ConnectionState::Login => {
self.try_send_packet(CLoginDisconnect::new(
self.try_send_packet(&CLoginDisconnect::new(
&serde_json::to_string_pretty(&reason).unwrap(),
))
.unwrap_or_else(|_| self.close());
}
ConnectionState::Config => {
self.try_send_packet(CConfigDisconnect::new(reason))
self.try_send_packet(&CConfigDisconnect::new(reason))
.unwrap_or_else(|_| self.close());
}
ConnectionState::Play => {
self.try_send_packet(CPlayDisconnect::new(TextComponent {
text: reason.to_string(),
}))
self.try_send_packet(&CPlayDisconnect::new(TextComponent::from(reason)))
.unwrap_or_else(|_| self.close());
}
_ => {

View File

@ -1,20 +1,23 @@
use num_traits::FromPrimitive;
use pumpkin_inventory::WindowType;
use pumpkin_protocol::{
client::play::{
Animation, CEntityAnimation, CHeadRot, CSystemChatMessge, CUpdateEntityPos,
CUpdateEntityPosRot, CUpdateEntityRot,
Animation, CEntityAnimation, CHeadRot, COpenScreen, CUpdateEntityPos, CUpdateEntityPosRot,
CUpdateEntityRot,
},
server::play::{
SChatCommand, SChatMessage, SConfirmTeleport, SPlayerCommand, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SSwingArm,
},
text::TextComponent,
VarInt,
};
use pumpkin_text::TextComponent;
use crate::{
commands::{handle_command, CommandSender},
entity::player::Hand,
server::Server,
util::math::wrap_degrees,
};
use super::Client;
@ -38,18 +41,27 @@ impl Client {
}
}
fn clamp_horizontal(pos: f64) -> f64 {
pos.clamp(-3.0E7, 3.0E7)
}
fn clamp_vertical(pos: f64) -> f64 {
pos.clamp(-2.0E7, 2.0E7)
}
pub fn handle_position(&mut self, server: &mut Server, position: SPlayerPosition) {
if position.x.is_nan() || position.feet_y.is_nan() || position.z.is_nan() {
self.kick("Invalid movement");
return;
}
let player = self.player.as_mut().unwrap();
let entity = &mut player.entity;
entity.lastx = entity.x;
entity.lasty = entity.y;
entity.lastz = entity.z;
entity.x = position.x;
entity.y = position.feet_y;
entity.z = position.z;
entity.x = Self::clamp_horizontal(position.x);
entity.y = Self::clamp_vertical(position.feet_y);
entity.z = Self::clamp_horizontal(position.z);
// TODO: teleport when moving > 8 block
// send new position to all other players
@ -61,7 +73,7 @@ impl Client {
server.broadcast_packet(
self,
CUpdateEntityPos::new(
&CUpdateEntityPos::new(
entity_id.into(),
(x * 4096.0 - lastx * 4096.0) as i16,
(y * 4096.0 - lasty * 4096.0) as i16,
@ -81,18 +93,23 @@ impl Client {
|| position_rotation.z.is_nan()
{
self.kick("Invalid movement");
return;
}
if !position_rotation.yaw.is_finite() || !position_rotation.pitch.is_finite() {
self.kick("Invalid rotation");
return;
}
let player = self.player.as_mut().unwrap();
let entity = &mut player.entity;
entity.x = position_rotation.x;
entity.y = position_rotation.feet_y;
entity.z = position_rotation.z;
entity.yaw = position_rotation.yaw % 360.0;
entity.pitch = position_rotation.pitch.clamp(-90.0, 90.0) % 360.0;
entity.lastx = entity.x;
entity.lasty = entity.y;
entity.lastz = entity.z;
entity.x = Self::clamp_horizontal(position_rotation.x);
entity.y = Self::clamp_vertical(position_rotation.feet_y);
entity.z = Self::clamp_horizontal(position_rotation.z);
entity.yaw = wrap_degrees(position_rotation.yaw);
entity.pitch = wrap_degrees(position_rotation.pitch);
// send new position to all other players
let on_ground = player.on_ground;
@ -106,7 +123,7 @@ impl Client {
server.broadcast_packet(
self,
CUpdateEntityPosRot::new(
&CUpdateEntityPosRot::new(
entity_id.into(),
(x * 4096.0 - lastx * 4096.0) as i16,
(y * 4096.0 - lasty * 4096.0) as i16,
@ -116,32 +133,34 @@ impl Client {
on_ground,
),
);
server.broadcast_packet(self, CHeadRot::new(entity_id.into(), head_yaw as u8));
server.broadcast_packet(self, &CHeadRot::new(entity_id.into(), head_yaw as u8));
}
pub fn handle_rotation(&mut self, server: &mut Server, rotation: SPlayerRotation) {
if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() {
self.kick("Invalid rotation");
return;
}
let player = self.player.as_mut().unwrap();
let entity = &mut player.entity;
entity.yaw = rotation.yaw % 360.0;
entity.pitch = rotation.pitch.clamp(-90.0, 90.0) % 360.0;
entity.yaw = wrap_degrees(rotation.yaw);
entity.pitch = wrap_degrees(rotation.pitch);
// send new position to all other players
let on_ground = player.on_ground;
let entity_id = entity.entity_id;
let yaw = entity.yaw;
let pitch = entity.pitch;
let yaw = (entity.yaw * 256.0 / 360.0).floor();
let pitch = (entity.pitch * 256.0 / 360.0).floor();
let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor();
server.broadcast_packet(
self,
CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, on_ground),
&CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, on_ground),
);
server.broadcast_packet(self, CHeadRot::new(entity_id.into(), yaw as u8));
server.broadcast_packet(self, &CHeadRot::new(entity_id.into(), head_yaw as u8));
}
pub fn handle_chat_command(&mut self, _server: &mut Server, command: SChatCommand) {
handle_command(&mut CommandSender::Player(self), command.command);
handle_command(&mut CommandSender::Player(self), &command.command);
}
pub fn handle_player_command(&mut self, _server: &mut Server, command: SPlayerCommand) {
@ -170,26 +189,31 @@ impl Client {
};
let player = self.player.as_mut().unwrap();
let id = player.entity_id();
server.broadcast_packet_expect(self, CEntityAnimation::new(id.into(), animation as u8))
server.broadcast_packet_expect(self, &CEntityAnimation::new(id.into(), animation as u8))
}
pub fn handle_chat_message(&mut self, server: &mut Server, chat_message: SChatMessage) {
let message = chat_message.message;
self.send_packet(&COpenScreen::new(
VarInt(0),
VarInt(WindowType::CraftingTable as i32),
TextComponent::from("Test Crafter"),
));
// TODO: filter message & validation
let gameprofile = self.gameprofile.as_ref().unwrap();
dbg!("got message");
// yeah a "raw system message", the ugly way to do that, but it works
server.broadcast_packet(
self,
CSystemChatMessge::new(
TextComponent::from(format!("{}: {}", gameprofile.name, message)),
false,
),
)
// server.broadcast_packet(
// self,
// CSystemChatMessge::new(
// TextComponent::from(format!("{}: {}", gameprofile.name, message)),
// false,
// ),
// );
/* server.broadcast_packet(
self,
CPlayerChatMessage::new(
gameprofile.id,
pumpkin_protocol::uuid::UUID(gameprofile.id),
0.into(),
None,
message.clone(),
@ -202,7 +226,9 @@ impl Client {
TextComponent::from(gameprofile.name.clone()),
None,
),
) */
)
*/
/* server.broadcast_packet(
self,
CDisguisedChatMessage::new(

View File

@ -2,12 +2,6 @@ use super::Command;
pub struct GamemodeCommand {}
impl GamemodeCommand {
pub fn new() -> Self {
Self {}
}
}
impl<'a> Command<'a> for GamemodeCommand {
const NAME: &'a str = "gamemode";

View File

@ -1,6 +1,6 @@
use gamemode::GamemodeCommand;
use pumpkin::PumpkinCommand;
use pumpkin_protocol::text::TextComponent;
use pumpkin_text::TextComponent;
use crate::client::Client;
@ -23,6 +23,7 @@ pub trait Command<'a> {
}
pub enum CommandSender<'a> {
Rcon(&'a mut Vec<String>),
Console,
Player(&'a mut Client),
}
@ -31,8 +32,9 @@ impl<'a> CommandSender<'a> {
pub fn send_message(&mut self, text: TextComponent) {
match self {
// TODO: add color and stuff to console
CommandSender::Console => log::info!("{}", text.text),
CommandSender::Console => log::info!("{:?}", text.content),
CommandSender::Player(c) => c.send_system_message(text),
CommandSender::Rcon(s) => s.push(format!("{:?}", text.content)),
}
}
@ -40,6 +42,7 @@ impl<'a> CommandSender<'a> {
match self {
CommandSender::Console => false,
CommandSender::Player(_) => true,
CommandSender::Rcon(_) => false,
}
}
@ -47,16 +50,18 @@ impl<'a> CommandSender<'a> {
match self {
CommandSender::Console => true,
CommandSender::Player(_) => false,
CommandSender::Rcon(_) => true,
}
}
pub fn as_mut_player(&mut self) -> Option<&mut Client> {
match self {
CommandSender::Player(client) => Some(client),
CommandSender::Console => None,
CommandSender::Rcon(_) => None,
}
}
}
pub fn handle_command(sender: &mut CommandSender, command: String) {
pub fn handle_command(sender: &mut CommandSender, command: &str) {
let command = command.to_lowercase();
// an ugly mess i know
if command.starts_with(PumpkinCommand::NAME) {

View File

@ -1,4 +1,5 @@
use pumpkin_protocol::{text::TextComponent, CURRENT_MC_PROTOCOL};
use pumpkin_protocol::CURRENT_MC_PROTOCOL;
use pumpkin_text::{color::NamedColor, TextComponent};
use crate::server::CURRENT_MC_VERSION;
@ -6,12 +7,6 @@ use super::Command;
pub struct PumpkinCommand {}
impl PumpkinCommand {
pub fn new() -> Self {
Self {}
}
}
impl<'a> Command<'a> for PumpkinCommand {
const NAME: &'a str = "pumpkin";
@ -20,6 +15,6 @@ impl<'a> Command<'a> for PumpkinCommand {
fn on_execute(sender: &mut super::CommandSender<'a>, _command: String) {
let version = env!("CARGO_PKG_VERSION");
let description = env!("CARGO_PKG_DESCRIPTION");
sender.send_message(TextComponent::from(format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")))
sender.send_message(TextComponent::from(format!("Pumpkin {version}, {description} (Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL})")).color_named(NamedColor::Green).bold())
}
}

View File

@ -5,7 +5,7 @@ use crate::client::authentication::ProfileAction;
#[derive(Deserialize, Serialize)]
pub struct Authentication {
/// Whether to use Mojang authentication.
pub use_authentication: bool,
pub enabled: bool,
/// Prevent proxy connections.
pub prevent_proxy_connections: bool,
@ -84,8 +84,8 @@ impl Default for TextureTypes {
impl Default for Authentication {
fn default() -> Self {
Self {
use_authentication: true,
prevent_proxy_connections: true,
enabled: true,
prevent_proxy_connections: false,
player_profile: Default::default(),
textures: Default::default(),
}

View File

@ -1,11 +1,13 @@
use std::path::Path;
use auth_config::Authentication;
use resource_pack::ResourcePack;
use serde::{Deserialize, Serialize};
use crate::{entity::player::GameMode, server::Difficulty};
pub mod auth_config;
pub mod resource_pack;
/// Current Config version of the Base Config
const CURRENT_BASE_VERSION: &str = "1.0.0";
@ -18,6 +20,27 @@ pub struct AdvancedConfiguration {
pub commands: Commands,
pub authentication: Authentication,
pub packet_compression: Compression,
pub resource_pack: ResourcePack,
pub rcon: RCONConfig,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct RCONConfig {
pub enabled: bool,
pub ip: String,
pub port: u16,
pub password: String,
}
impl Default for RCONConfig {
fn default() -> Self {
Self {
enabled: false,
ip: "0.0.0.0".to_string(),
port: 25575,
password: "".to_string(),
}
}
}
#[derive(Deserialize, Serialize)]
@ -63,6 +86,8 @@ impl Default for AdvancedConfiguration {
authentication: Authentication::default(),
commands: Commands::default(),
packet_compression: Compression::default(),
resource_pack: ResourcePack::default(),
rcon: RCONConfig::default(),
}
}
}
@ -83,10 +108,6 @@ pub struct BasicConfiguration {
pub view_distance: u8,
/// The maximum simulated view distance.
pub simulation_distance: u8,
/// The path to the resource pack.
pub resource_pack: String,
/// The SHA1 hash of the resource pack.
pub resource_pack_sha1: String,
/// The default game difficulty.
pub default_difficulty: Difficulty,
/// Whether the Nether dimension is enabled.
@ -107,14 +128,12 @@ impl Default for BasicConfiguration {
fn default() -> Self {
Self {
config_version: CURRENT_BASE_VERSION.to_string(),
server_address: "127.0.0.1".to_string(),
server_address: "0.0.0.0".to_string(),
server_port: 25565,
seed: "".to_string(),
max_players: 100000,
view_distance: 10,
simulation_distance: 10,
resource_pack: "".to_string(),
resource_pack_sha1: "".to_string(),
default_difficulty: Difficulty::Normal,
allow_nether: true,
hardcore: false,
@ -130,21 +149,30 @@ impl AdvancedConfiguration {
pub fn load<P: AsRef<Path>>(path: P) -> AdvancedConfiguration {
if path.as_ref().exists() {
let toml = std::fs::read_to_string(path).expect("Couldn't read configuration");
toml::from_str(toml.as_str()).expect("Couldn't parse, Proberbly old config")
let config: AdvancedConfiguration =
toml::from_str(toml.as_str()).expect("Couldn't parse features.toml, Proberbly old config, Replacing with a new one or just delete it");
config.validate();
config
} else {
let config = AdvancedConfiguration::default();
let toml = toml::to_string(&config).expect("Couldn't create toml!");
std::fs::write(path, toml).expect("Couldn't save configuration");
config.validate();
config
}
}
pub fn validate(&self) {
self.resource_pack.validate()
}
}
impl BasicConfiguration {
pub fn load<P: AsRef<Path>>(path: P) -> BasicConfiguration {
if path.as_ref().exists() {
let toml = std::fs::read_to_string(path).expect("Couldn't read configuration");
toml::from_str(toml.as_str()).expect("Couldn't parse")
let config: BasicConfiguration = toml::from_str(toml.as_str()).expect("Couldn't parse configuration.toml, Proberbly old config, Replacing with a new one or just delete it");
config.validate();
config
} else {
let config = BasicConfiguration::default();
let toml = toml::to_string(&config).expect("Couldn't create toml!");
@ -170,10 +198,5 @@ impl BasicConfiguration {
"When Online Mode is enabled, Encryption must be enabled"
)
}
assert_eq!(
!self.resource_pack.is_empty(),
!self.resource_pack_sha1.is_empty(),
"Resource Pack path or Sha1 hash is missing"
);
}
}

View File

@ -0,0 +1,40 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct ResourcePack {
pub enabled: bool,
/// The path to the resource pack.
pub resource_pack_url: String,
/// The SHA1 hash (40) of the resource pack.
pub resource_pack_sha1: String,
/// Custom propmt Text component, Leave blank for none
pub prompt_message: String,
/// Will force the Player to accept the resource pack
pub force: bool,
}
impl ResourcePack {
pub fn validate(&self) {
assert_eq!(
!self.resource_pack_url.is_empty(),
!self.resource_pack_sha1.is_empty(),
"Resource Pack path or Sha1 hash is missing"
);
assert!(
self.resource_pack_sha1.len() <= 40,
"Resource pack sha1 hash is too long (max. 40)"
)
}
}
impl Default for ResourcePack {
fn default() -> Self {
Self {
enabled: false,
resource_pack_url: "".into(),
resource_pack_sha1: "".into(),
force: false,
prompt_message: "".into(),
}
}
}

View File

@ -18,6 +18,7 @@ pub mod client;
pub mod commands;
pub mod config;
pub mod entity;
pub mod rcon;
pub mod server;
pub mod util;
@ -27,6 +28,8 @@ async fn main() -> io::Result<()> {
const SERVER: Token = Token(0);
use std::{cell::RefCell, time::Instant};
use rcon::RCONServer;
let time = Instant::now();
let basic_config = BasicConfiguration::load("configuration.toml");
@ -58,6 +61,7 @@ async fn main() -> io::Result<()> {
let mut unique_token = Token(SERVER.0 + 1);
let use_console = advanced_configuration.commands.use_console;
let rcon = advanced_configuration.rcon.clone();
let mut connections: HashMap<Token, Rc<RefCell<Client>>> = HashMap::new();
@ -73,10 +77,15 @@ async fn main() -> io::Result<()> {
stdin
.read_line(&mut out)
.expect("Failed to read console line");
handle_command(&mut commands::CommandSender::Console, out);
handle_command(&mut commands::CommandSender::Console, &out);
}
});
}
if rcon.enabled {
tokio::spawn(async move {
RCONServer::new(&rcon).await.unwrap();
});
}
loop {
if let Err(err) = poll.poll(&mut events, None) {
if interrupted(&err) {

220
pumpkin/src/rcon/mod.rs Normal file
View File

@ -0,0 +1,220 @@
use std::{
collections::HashMap,
io::{self, Read},
};
use mio::{
net::{TcpListener, TcpStream},
Events, Interest, Poll, Token,
};
use packet::{Packet, PacketError, PacketType};
use thiserror::Error;
use crate::{commands::handle_command, config::RCONConfig};
mod packet;
#[derive(Debug, Error)]
pub enum RCONError {
#[error("authentication failed")]
Auth,
#[error("command exceeds the maximum length")]
CommandTooLong,
#[error("{}", _0)]
Io(io::Error),
}
const SERVER: Token = Token(0);
pub struct RCONServer {}
impl RCONServer {
pub async fn new(config: &RCONConfig) -> Result<Self, io::Error> {
assert!(config.enabled, "RCON is not enabled");
let addr = format!("{}:{}", config.ip, config.port)
.parse()
.expect("Failed to parse RCON address");
let mut poll = Poll::new().unwrap();
let mut listener = TcpListener::bind(addr).unwrap();
poll.registry()
.register(&mut listener, SERVER, Interest::READABLE)
.unwrap();
let mut unique_token = Token(SERVER.0 + 1);
let mut events = Events::with_capacity(128);
let mut connections: HashMap<Token, RCONClient> = HashMap::new();
let password = config.password.clone();
loop {
poll.poll(&mut events, None).unwrap();
for event in events.iter() {
match event.token() {
SERVER => loop {
// Received an event for the TCP server socket, which
// indicates we can accept an connection.
let (mut connection, address) = match listener.accept() {
Ok((connection, address)) => (connection, address),
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
// If we get a `WouldBlock` error we know our
// listener has no more incoming connections queued,
// so we can return to polling and wait for some
// more.
break;
}
Err(e) => {
// If it was any other kind of error, something went
// wrong and we terminate with an error.
return Err(e);
}
};
log::info!("Accepted connection from: {}", address);
let token = Self::next(&mut unique_token);
poll.registry()
.register(
&mut connection,
token,
Interest::READABLE.add(Interest::WRITABLE),
)
.unwrap();
connections.insert(token, RCONClient::new(connection));
},
token => {
let done = if let Some(client) = connections.get_mut(&token) {
client.handle(&password).await
} else {
false
};
if done {
if let Some(mut client) = connections.remove(&token) {
poll.registry().deregister(&mut client.connection)?;
}
}
}
}
}
}
}
fn next(current: &mut Token) -> Token {
let next = current.0;
current.0 += 1;
Token(next)
}
}
pub struct RCONClient {
connection: TcpStream,
logged_in: bool,
incoming: Vec<u8>,
closed: bool,
}
impl RCONClient {
pub fn new(connection: TcpStream) -> Self {
Self {
connection,
logged_in: false,
incoming: Vec::new(),
closed: false,
}
}
pub async fn handle(&mut self, password: &str) -> bool {
if !self.closed {
loop {
match self.read_bytes() {
// Stream closed, so we can't reply, so we just close everything.
Ok(true) => return true,
Ok(false) => {}
Err(e) if e.kind() == io::ErrorKind::WouldBlock => break,
Err(e) => {
log::error!("could not read packet: {e}");
return true;
}
}
}
// If we get a close here, we might have a reply, which we still want to write.
match self.poll(password).await {
Ok(()) => {}
Err(e) => {
log::error!("rcon error: {e}");
self.closed = true;
}
}
}
self.closed
}
async fn poll(&mut self, password: &str) -> Result<(), PacketError> {
loop {
let packet = match self.receive_packet().await? {
Some(p) => p,
None => return Ok(()),
};
match packet.get_type() {
PacketType::Auth => {
let body = packet.get_body();
if !body.is_empty() && packet.get_body() == password {
self.send(&mut Packet::new(
packet.get_id(),
PacketType::AuthResponse,
"".into(),
))
.await
.unwrap();
log::info!("RCON Client logged in successfully");
self.logged_in = true;
} else {
log::warn!("RCON Client has tried wrong password");
self.send(&mut Packet::new(-1, PacketType::AuthResponse, "".into()))
.await
.unwrap();
return Err(PacketError::WrongPassword);
}
}
PacketType::ExecCommand => {
if self.logged_in {
let mut output = Vec::new();
handle_command(
&mut crate::commands::CommandSender::Rcon(&mut output),
packet.get_body(),
);
for line in output {
self.send(&mut Packet::new(packet.get_id(), PacketType::Output, line))
.await
.unwrap();
}
}
}
PacketType::Output => todo!(),
PacketType::AuthResponse => unreachable!(),
}
}
}
fn read_bytes(&mut self) -> io::Result<bool> {
let mut buf = [0; 1460];
let n = self.connection.read(&mut buf)?;
if n == 0 {
return Ok(true);
}
self.incoming.extend_from_slice(&buf[..n]);
Ok(false)
}
async fn send(&mut self, packet: &mut Packet) -> io::Result<()> {
packet.send_packet(&mut self.connection).await
}
async fn receive_packet(&mut self) -> Result<Option<Packet>, PacketError> {
Packet::deserialize(&mut self.incoming).await
}
}

113
pumpkin/src/rcon/packet.rs Normal file
View File

@ -0,0 +1,113 @@
use std::io::{self, BufRead, Cursor, Write};
use bytes::{BufMut, BytesMut};
use mio::net::TcpStream;
use thiserror::Error;
use tokio::io::AsyncReadExt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PacketType {
Auth,
AuthResponse,
ExecCommand,
Output,
}
#[derive(Error, Debug)]
pub enum PacketError {
#[error("invalid length")]
InvalidLength,
#[error("expected terminating NUL byte")]
NoNullTermination,
#[error("wrong password")]
WrongPassword,
}
impl PacketType {
fn to_i32(self) -> i32 {
match self {
PacketType::Auth => 3,
PacketType::AuthResponse => 2,
PacketType::ExecCommand => 2,
PacketType::Output => 0,
}
}
pub fn from_i32(n: i32) -> PacketType {
match n {
3 => PacketType::Auth,
2 => PacketType::ExecCommand,
_ => PacketType::Output,
}
}
}
#[derive(Debug)]
pub struct Packet {
id: i32,
ptype: PacketType,
body: String,
}
impl Packet {
pub fn new(id: i32, ptype: PacketType, body: String) -> Packet {
Packet { id, ptype, body }
}
pub async fn send_packet(&mut self, connection: &mut TcpStream) -> io::Result<()> {
// let len = outgoing.len() as u64;
let mut buf = BytesMut::new();
// 10 is for 4 bytes ty, 4 bytes id, and 2 terminating nul bytes.
buf.put_i32_le(10 + self.get_body().len() as i32);
buf.put_i32_le(self.id);
buf.put_i32_le(self.get_type().to_i32());
let bytes = self.get_body().as_bytes();
buf.put_slice(bytes);
buf.put_u8(0);
buf.put_u8(0);
connection.write(&buf).unwrap();
Ok(())
}
pub async fn deserialize(incoming: &mut Vec<u8>) -> Result<Option<Packet>, PacketError> {
if incoming.len() < 4 {
return Ok(None);
}
let mut buf = Cursor::new(&incoming);
let len = buf.read_i32_le().await.unwrap() + 4;
if !(0..=1460).contains(&len) {
return Err(PacketError::InvalidLength);
}
let id = buf.read_i32_le().await.unwrap();
let ty = buf.read_i32_le().await.unwrap();
let mut payload = vec![];
let _ = buf.read_until(b'\0', &mut payload).unwrap();
payload.pop();
if buf.read_u8().await.unwrap() != 0 {
return Err(PacketError::NoNullTermination);
}
if buf.position() != len as u64 {
return Err(PacketError::InvalidLength);
}
incoming.drain(0..len as usize);
let packet = Packet {
id,
ptype: PacketType::from_i32(ty),
body: String::from_utf8(payload).unwrap(),
};
Ok(Some(packet))
}
pub fn get_body(&self) -> &str {
&self.body
}
pub fn get_type(&self) -> PacketType {
self.ptype
}
pub fn get_id(&self) -> i32 {
self.id
}
}

View File

@ -4,6 +4,7 @@ use std::{
io::Cursor,
rc::Rc,
sync::atomic::{AtomicI32, Ordering},
time::Duration,
};
use base64::{engine::general_purpose, Engine};
@ -21,6 +22,9 @@ use pumpkin_protocol::{
}, BitSet, ClientPacket, Players, Sample, StatusResponse, VarInt, Version, CURRENT_MC_PROTOCOL
};
use pumpkin_world::dimension::Dimension;
use pumpkin_registry::Registry;
use pumpkin_world::chunk::TestChunk;
use rsa::{traits::PublicKeyParts, RsaPrivateKey, RsaPublicKey};
use serde::{Deserialize, Serialize};
@ -48,6 +52,9 @@ pub struct Server {
/// Cache the Server brand buffer so we don't have to rebuild them every time a player joins
pub cached_server_brand: Vec<u8>,
/// Cache the registry so we don't have to parse it every time a player joins
pub cached_registry: Vec<Registry>,
pub current_clients: HashMap<Rc<Token>, Rc<RefCell<Client>>>,
// TODO: replace with HashMap <World, Player>
@ -76,12 +83,18 @@ impl Server {
)
.into_boxed_slice();
let auth_client = if config.0.online_mode {
Some(reqwest::Client::new())
Some(
reqwest::Client::builder()
.timeout(Duration::from_millis(5000))
.build()
.expect("Failed to to make reqwest client"),
)
} else {
None
};
Self {
cached_registry: Registry::get_static(),
// 0 is invalid
entity_id: 2.into(),
// world: World::load(""),
@ -116,7 +129,12 @@ impl Server {
// todo: put this into the entitiy struct
if client.is_player() {
let id = client.player.as_ref().unwrap().entity_id();
self.broadcast_packet(&mut client, CRemoveEntities::new(vec![id.into()]))
let uuid = client.gameprofile.as_ref().unwrap().id;
self.broadcast_packet_expect(
&mut client,
&CRemovePlayerInfo::new(1.into(), &[UUID(uuid)]),
);
self.broadcast_packet_expect(&mut client, &CRemoveEntities::new(&[id.into()]))
}
}
@ -130,10 +148,10 @@ impl Server {
client.player = Some(player);
// login packet for our new player
client.send_packet(CLogin::new(
client.send_packet(&CLogin::new(
entity_id,
self.base_config.hardcore,
vec!["minecraft:overworld".into()],
&["minecraft:overworld"],
self.base_config.max_players.into(),
self.base_config.view_distance.into(), // TODO: view distance
self.base_config.simulation_distance.into(), // TODO: sim view dinstance
@ -141,7 +159,7 @@ impl Server {
false,
false,
0.into(),
"minecraft:overworld".into(),
"minecraft:overworld",
0, // seed
match self.base_config.default_gamemode {
GameMode::Undefined => GameMode::Survival,
@ -158,26 +176,31 @@ impl Server {
));
dbg!("sending abilities");
// player abilities
client.send_packet(CPlayerAbilities::new(0x02, 00.2, 0.1));
client.send_packet(&CPlayerAbilities::new(0x02, 00.2, 0.1));
// teleport
let x = 10.0;
let y = 120.0;
let z = 10.0;
let yaw = 10.0;
let pitch = 10.0;
client.teleport(x, y, z, 10.0, 10.0);
let gameprofile = client.gameprofile.as_ref().unwrap();
// first send info update to our new player, So he can see his Skin
// also send his info to everyone else
self.broadcast_packet(
client,
CPlayerInfoUpdate::new(
0x01,
&CPlayerInfoUpdate::new(
0x01 | 0x08,
&[pumpkin_protocol::client::play::Player {
uuid: gameprofile.id,
actions: vec![PlayerAction::AddPlayer {
actions: vec![
PlayerAction::AddPlayer {
name: gameprofile.name.clone(),
properties: gameprofile.properties.clone(),
}],
},
PlayerAction::UpdateListed { listed: true },
],
}],
),
);
@ -190,37 +213,41 @@ impl Server {
let gameprofile = client.gameprofile.as_ref().unwrap();
entries.push(pumpkin_protocol::client::play::Player {
uuid: gameprofile.id,
actions: vec![PlayerAction::AddPlayer {
actions: vec![
PlayerAction::AddPlayer {
name: gameprofile.name.clone(),
properties: gameprofile.properties.clone(),
}],
},
PlayerAction::UpdateListed { listed: true },
],
})
}
}
client.send_packet(CPlayerInfoUpdate::new(0x01, &entries));
client.send_packet(&CPlayerInfoUpdate::new(0x01 | 0x08, &entries));
// Start waiting for level chunks
client.send_packet(CGameEvent::new(13, 0.0));
client.send_packet(&CGameEvent::new(13, 0.0));
let gameprofile = client.gameprofile.as_ref().unwrap();
// spawn player for every client
self.broadcast_packet_expect(
client,
CSpawnEntity::new(
// TODO: add velo
&CSpawnEntity::new(
entity_id.into(),
gameprofile.id,
UUID(gameprofile.id),
EntityType::Player.to_i32().unwrap().into(),
x,
y,
z,
0,
0,
0,
pitch,
yaw,
yaw,
0.into(),
0,
0,
0,
0.0,
0.0,
0.0,
),
);
// spawn players for our client
@ -230,64 +257,64 @@ impl Server {
if let Some(player) = &existing_client.player {
let entity = &player.entity;
let gameprofile = existing_client.gameprofile.as_ref().unwrap();
client.send_packet(CSpawnEntity::new(
client.send_packet(&CSpawnEntity::new(
player.entity_id().into(),
gameprofile.id,
UUID(gameprofile.id),
EntityType::Player.to_i32().unwrap().into(),
entity.x,
entity.y,
entity.z,
entity.yaw as u8,
entity.pitch as u8,
0,
entity.yaw,
entity.pitch,
entity.pitch,
0.into(),
0,
0,
0,
0.0,
0.0,
0.0,
))
}
}
// entity meta data
/* if let Some(config) = &client.config {
if let Some(config) = &client.config {
self.broadcast_packet(
client,
CSetEntityMetadata::new(entity_id.into(), vec![Metadata::new(18, VarInt(0), 0)]),
&CSetEntityMetadata::new(
entity_id.into(),
Metadata::new(17, VarInt(0), config.skin_parts),
),
)
}
*/
Server::spawn_test_chunk(client).await;
}
/// Sends a Packet to all Players
pub fn broadcast_packet<P>(&mut self, from: &mut Client, packet: P)
pub fn broadcast_packet<P>(&mut self, from: &mut Client, packet: &P)
where
P: ClientPacket,
P: Clone,
{
// we can't borrow twice at same time
from.send_packet(packet.clone());
from.send_packet(packet);
for (_, client) in self.current_clients.iter().filter(|c| c.0 != &from.token) {
// Check if client is a player
let mut client = client.borrow_mut();
if client.is_player() {
// we need to clone, Because we send a new packet to every client
client.send_packet(packet.clone());
client.send_packet(packet);
}
}
}
pub fn broadcast_packet_expect<P>(&mut self, from: &mut Client, packet: P)
pub fn broadcast_packet_expect<P>(&mut self, from: &mut Client, packet: &P)
where
P: ClientPacket,
P: Clone,
{
for (_, client) in self.current_clients.iter().filter(|c| c.0 != &from.token) {
// Check if client is a player
let mut client = client.borrow_mut();
if client.is_player() {
// we need to clone, Because we send a new packet to every client
client.send_packet(packet.clone());
client.send_packet(packet);
}
}
}
@ -363,7 +390,7 @@ impl Server {
pub fn send_brand(&self, client: &mut Client) {
// send server brand
client.send_packet(CPluginMessage::new(
client.send_packet(&CPluginMessage::new(
"minecraft:brand",
&self.cached_server_brand,
));

12
pumpkin/src/util/math.rs Normal file
View File

@ -0,0 +1,12 @@
pub fn wrap_degrees(var: f32) -> f32 {
let mut var1 = var % 360.0;
if var1 >= 180.0 {
var1 -= 360.0;
}
if var1 < -180.0 {
var1 += 360.0;
}
var1
}

View File

@ -1 +1 @@
pub mod math;