Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
5d6c38d1b2
34
Cargo.lock
generated
34
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
|
||||
@ -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"] }
|
||||
|
||||
12
README.md
12
README.md
@ -4,7 +4,7 @@
|
||||
|
||||

|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
|
||||
</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
|
||||
|
||||
|
||||
8
pumpkin-inventory/Cargo.toml
Normal file
8
pumpkin-inventory/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "pumpkin-inventory"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
num-traits = "0.2"
|
||||
num-derive = "0.4"
|
||||
41
pumpkin-inventory/src/lib.rs
Normal file
41
pumpkin-inventory/src/lib.rs
Normal 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,
|
||||
}
|
||||
@ -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"
|
||||
|
||||
31
pumpkin-protocol/src/client/config/c_add_resource_pack.rs
Normal file
31
pumpkin-protocol/src/client/config/c_add_resource_pack.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
15
pumpkin-protocol/src/client/play/c_actionbar.rs
Normal file
15
pumpkin-protocol/src/client/play/c_actionbar.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
@ -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)]
|
||||
|
||||
@ -4,7 +4,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
#[packet(0x03)]
|
||||
pub struct CEntityAnimation {
|
||||
entity_id: VarInt,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -3,7 +3,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
#[packet(0x48)]
|
||||
pub struct CHeadRot {
|
||||
entity_id: VarInt,
|
||||
|
||||
@ -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,
|
||||
|
||||
23
pumpkin-protocol/src/client/play/c_open_screen.rs
Normal file
23
pumpkin-protocol/src/client/play/c_open_screen.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
20
pumpkin-protocol/src/client/play/c_player_remove.rs
Normal file
20
pumpkin-protocol/src/client/play/c_player_remove.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
15
pumpkin-protocol/src/client/play/c_set_title.rs
Normal file
15
pumpkin-protocol/src/client/play/c_set_title.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
15
pumpkin-protocol/src/client/play/c_subtitle.rs
Normal file
15
pumpkin-protocol/src/client/play/c_subtitle.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -3,7 +3,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
#[packet(0x2F)]
|
||||
pub struct CUpdateEntityPosRot {
|
||||
entity_id: VarInt,
|
||||
|
||||
@ -3,7 +3,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
#[packet(0x2E)]
|
||||
pub struct CUpdateEntityPos {
|
||||
entity_id: VarInt,
|
||||
|
||||
@ -3,7 +3,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::VarInt;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[derive(Serialize)]
|
||||
#[packet(0x30)]
|
||||
pub struct CUpdateEntityRot {
|
||||
entity_id: VarInt,
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
}
|
||||
13
pumpkin-protocol/src/uuid.rs
Normal file
13
pumpkin-protocol/src/uuid.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
|
||||
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
9
pumpkin-text/Cargo.toml
Normal 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
19
pumpkin-text/src/click.rs
Normal 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
36
pumpkin-text/src/color.rs
Normal 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
32
pumpkin-text/src/hover.rs
Normal 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
167
pumpkin-text/src/lib.rs
Normal 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
100
pumpkin-text/src/style.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
@ -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}")
|
||||
};
|
||||
|
||||
@ -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(
|
||||
®istry.registry_id,
|
||||
®istry.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(
|
||||
|
||||
@ -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());
|
||||
}
|
||||
_ => {
|
||||
|
||||
@ -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(
|
||||
// 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(
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
|
||||
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
40
pumpkin/src/config/resource_pack.rs
Normal file
40
pumpkin/src/config/resource_pack.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
220
pumpkin/src/rcon/mod.rs
Normal 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
113
pumpkin/src/rcon/packet.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
12
pumpkin/src/util/math.rs
Normal 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
|
||||
}
|
||||
@ -1 +1 @@
|
||||
|
||||
pub mod math;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user