Merge branch 'master' into pumpkinerror-trait

This commit is contained in:
Alexander Medvedev
2024-10-13 16:59:23 +01:00
committed by GitHub
118 changed files with 7711 additions and 2031 deletions

View File

@@ -6,6 +6,7 @@
# Allow the source code folders
!/pumpkin*/
!/assets
# Dependencies
!Cargo.lock

View File

@@ -32,7 +32,7 @@ jobs:
node-version: 20
cache: npm
- name: Setup Pages
uses: actions/configure-pages@v4
uses: actions/configure-pages@v5
- name: Install dependencies
run: npm ci
- name: Build with VitePress

View File

@@ -39,9 +39,8 @@ And in release:
cargo run --no-default-features --release
```
### Project Structure
Before contributing, it would be helpful to get to know the project structure, for further information, visit [STRUCTURE.md](STRUCTURE.md)
### Docs
The Documentation of Pumpkin can be found at https://snowiiii.github.io/Pumpkin/
### Additional Information

514
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -25,14 +25,21 @@ codegen-units = 1
[workspace.dependencies]
log = "0.4"
tokio = { version = "1.40", features = [
"net",
"macros",
"rt-multi-thread",
"fs",
"io-util",
"macros",
"net",
"rt-multi-thread",
"sync",
] }
# Concurrency/Parallelism and Synchronization
rayon = "1.10.0"
parking_lot = "0.12.3"
crossbeam = "0.8.4"
uuid = { version = "1.10.0", features = ["serde", "v3", "v4"] }
derive_more = { version = "1.0.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
itertools = "0.13.0"

View File

@@ -1,4 +1,4 @@
FROM rust:1-alpine3.19 AS builder
FROM rust:1-alpine3.20 AS builder
ENV RUSTFLAGS="-C target-feature=-crt-static -C target-cpu=native"
RUN apk add --no-cache musl-dev
WORKDIR /pumpkin
@@ -6,8 +6,9 @@ COPY . /pumpkin
RUN cargo build --release
RUN strip target/release/pumpkin
FROM alpine:3.19
FROM alpine:3.20
WORKDIR /pumpkin
RUN apk add --no-cache libgcc
COPY --from=builder /pumpkin/target/release/pumpkin /pumpkin/pumpkin
EXPOSE 25565
ENTRYPOINT ["/pumpkin/pumpkin"]

View File

@@ -9,7 +9,7 @@
</div>
Pumpkin is a Minecraft server built entirely in Rust, offering a fast, efficient,
[Pumpkin](https://snowiiii.github.io/Pumpkin/) is a Minecraft server built entirely in Rust, offering a fast, efficient,
and customizable experience. It prioritizes performance and player enjoyment while adhering to the core mechanics of the game.
![image](https://github.com/user-attachments/assets/7e2e865e-b150-4675-a2d5-b52f9900378e)
@@ -24,7 +24,8 @@ and customizable experience. It prioritizes performance and player enjoyment whi
## What Pumpkin will not
- Provide compatibility with Vanilla or Bukkit servers (including configs and plugins).
- Be a drop-in replacement for vanilla or other servers
- Be compatible with plugins or mods for other servers
- Function as a framework for building a server from scratch.
> [!IMPORTANT]
@@ -73,55 +74,22 @@ and customizable experience. It prioritizes performance and player enjoyment whi
Check out our [Github Project](https://github.com/users/Snowiiii/projects/12/views/3) to see current progress
## How to run
There are currently no release builds, because there was no release :D.
To get Pumpkin running you first have to clone it:
```shell
git clone https://github.com/Snowiiii/Pumpkin.git
cd Pumpkin
```
You also may have to [install rust](https://www.rust-lang.org/tools/install) when you don't already have.
You can place a vanilla world into the Pumpkin/ directory when you want. Just name the World to `world`
Then run:
> [!NOTE]
> This can take a while. Because we enabled heavy optimizations for release builds
>
> To apply further optimizations specfic to your CPU and use your CPU features. You should set the target-cpu=native
> Rust flag.
```shell
cargo run --release
```
### Docker
Experimental Docker support is available.
The image is currently not published anywhere, but you can use the following command to build it:
```shell
docker build . -t pumpkin
```
To run it use the following command:
```shell
docker run --rm -v "./world:/pumpkin/world" pumpkin
```
See https://snowiiii.github.io/Pumpkin/about/quick-start.html
## Contributions
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md)
## Docs
The Documentation of Pumpkin can be found at https://snowiiii.github.io/Pumpkin/
## Communication
Consider joining our [discord](https://discord.gg/wT8XjrjKkf) to stay up-to-date on events, updates, and connect with other members.
## Funding
If you want to fund me and help the project, Check out my [GitHub sponsors](https://github.com/sponsors/Snowiiii)
## Thanks
A big thanks to [wiki.vg](https://wiki.vg/) for providing valuable information used in the development of this project.

View File

@@ -1,17 +0,0 @@
# Project Structure
## Overview
Pumpkin is split into multiple crates, thus having a set project structure between contributors is essential.
## Pumpkin-Core
The core crate has some special rules that only apply to it:
- It may not depend on any other pumpkin crate
- There may not be any files directly under src/, except for the mod.rs file (this is to help with organisation)
## Other crate rules
- [`pumpkin-protocol`](/pumpkin-protocol/) - contains definitions for packet types **and** their serialization (be it through serde, or manually implementing `ClientPacket`/`ServerPacket`), only the `pumpkin` crate may depend on this
- `pumpkin-macros` - similarly to `pumpkin-core`, it may not depend on any other pumpkin crate

View File

@@ -9,26 +9,42 @@ export default defineConfig({
base: "/Pumpkin/",
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
search: {
provider: "local",
},
sidebar: [
{
text: "About",
items: [
{ text: "Introduction", link: "/about/introduction" },
{ text: "Quick Start", link: "/about/quick-start" },
],
},
{
text: "Developers",
items: [
{
text: "Contributing",
link: "https://github.com/Snowiiii/Pumpkin/blob/master/CONTRIBUTING.md",
},
{ text: "Introduction", link: "/developer/introduction" },
{ text: "Networking", link: "/developer/networking" },
{ text: "Authentication", link: "/developer/authentication" },
],
},
{
text: "Configuration",
items: [
{ text: "Introduction", link: "/config/introduction" },
{ text: "Basic", link: "/config/basic" },
{ text: "Advanced", link: "/config/advanced" },
],
},
{
text: "Plugins",
text: "Troubleshooting",
items: [
{ text: "About Plugins", link: "/plugins/about" },
{
text: "Getting Started in Rust",
link: "/plugins/getting-started-rs",
},
{ text: "Common Issues", link: "/troubleshooting/common_issues.md" },
],
},
],
@@ -39,6 +55,22 @@ export default defineConfig({
],
logo: "/assets/icon.png",
footer: {
message: "Released under the MIT License.",
copyright: "Copyright © 2024-present Aleksandr Medvedev",
},
editLink: {
pattern: "https://github.com/Snowiiii/Pumpkin/blob/master/docs/:path",
text: "Edit this page on GitHub",
},
lastUpdated: {
text: "Updated at",
formatOptions: {
dateStyle: "medium",
timeStyle: "medium",
},
},
outline: "deep"
},
head: [["link", { rel: "icon", href: "/assets/favicon.ico" }]],
});

View File

@@ -15,7 +15,8 @@ and customizable experience. It prioritizes performance and player enjoyment whi
## What Pumpkin will not
- Provide compatibility with Vanilla or Bukkit servers (including configs and plugins).
- Be a drop-in replacement for vanilla or other servers
- Be compatible with plugins or mods for other servers
- Function as a framework for building a server from scratch.
> [!IMPORTANT]

427
docs/config/advanced.md Normal file
View File

@@ -0,0 +1,427 @@
### Advanced Configuration
### Proxy
`proxy`
Wether Proxy Configuration is enabled
```toml
enabled=false
```
#### Velocity
`proxy.velocity`
Wether [Velocity](https://papermc.io/software/velocity) Proxy is enabled
> [!IMPORTANT]
> Velocity support is currently WIP
```toml
enabled=false
```
##### Velocity Secret
This secret is used to ensure that player info forwarded by Velocity comes from your proxy and not from someone pretending to run Velocity
```toml
secret=
```
### Authentication
`authentication`
Wether Authentication is enabled
```toml
enabled=false
```
#### Authentication URL
The Authentication URL being used
> [!IMPORTANT]
> {username} | The Username from the requested player
>
> {server_hash} | The SHA1 Encrypted hash
```toml
auth_url="https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}"
```
#### Prevent Proxy Connections
Prevent proxy connections
```toml
prevent_proxy_connections=false
```
#### Prevent Proxy Connections URL
The Authentication URL being used
> [!IMPORTANT]
> {username} | The Username from the requested player
>
> {server_hash} | The SHA1 Encrypted hash
>
> {ip} | The IP of the requested Player
```toml
prevent_proxy_connection_auth_url = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}"
```
#### Player Profile
`authentication.player_profile`
##### Allow Banned Players
Allow players flagged by Mojang (banned, forced name change)
```toml
allow_banned_players=false
```
##### Allowed Actions
Depends on the value above
```toml
allowed_actions=["FORCED_NAME_CHANGE", "USING_BANNED_SKIN"]
```
```toml
FORCED_NAME_CHANGE
USING_BANNED_SKIN
```
#### Textures
`authentication.textures`
Whether to filter/validate player textures (e.g. Skins/Capes)
```toml
enabled=true
```
##### Allowed URL Schemes
Allowed URL Schemes for Textures
```toml
allowed_url_schemes=["http", "https"]
```
##### Allowed URL Domains
Allowed URL domains for Textures
```toml
allowed_url_domains=[".minecraft.net", ".mojang.com"]
```
#### Texture Types
`authentication.textures.types`
##### Skin
Use player skins
```toml
skin=true
```
##### Cape
Use player capes
```toml
cape=true
```
##### Elytra
Use player elytras
(i didn't know myself that there are custom elytras)
```toml
elytra=true
```
### Compression
`packet_compression`
Wether Packet Compression is enabled
```toml
enable=true
```
#### Compression Info
##### Threshold
The compression threshold used when compression is enabled
```toml
threshold=256
```
##### Level
The Compression Level
> [!IMPORTANT]
> A value between 0..9
>
> 1 = Optimize for the best speed of encoding.
>
> 9 = Optimize for the size of data being encoded.
```toml
level=4
```
### Resource Pack
`resource_pack`
Wether a Resource Pack is enabled
```toml
enable=false
```
#### Resource Pack URL
The download URL of the resource pack
```toml
resource_pack_url=
```
#### Resource Pack SHA1
The SHA1 hash (40) of the resource pack
```toml
resource_pack_sha1=
```
#### Prompt Message
Custom prompt Text component, Leave blank for none
```toml
prompt_message=
```
#### Force
Will force the Player to accept the resource pack
```toml
force=false
```
### Commands
`commands`
#### Use Console
Are commands from the Console accepted
```toml
use_console=true
```
#### Log Console
Should be commands from players be logged in console
```toml
log_console=true
```
### RCON Config
`rcon`
Wether RCON is enabled
```toml
enable=false
```
#### Address
The network address and port where the RCON server will listen for connections
```toml
address=false
```
#### Password
The password required for RCON authentication
```toml
password=
```
#### Maximum Connections
The maximum number of concurrent RCON connections allowed
If 0 there is no limit
```toml
max_connections=0
```
#### RCON Logging
`rcon.logging`
##### Logged Successfully
Whether successful RCON logins should be logged
```toml
log_logged_successfully=true
```
##### Wrong Password
Whether failed RCON login attempts with incorrect passwords should be logged
```toml
log_wrong_password=true
```
##### Commands
Whether all RCON commands, regardless of success or failure, should be logged
```toml
log_commands=true
```
##### Disconnect
Whether RCON client quit should be logged
```toml
log_quit=true
```
### PVP
`pvp`
Whether PVP is enabled
```toml
enable=true
```
#### Hurt Animation
Do we want to have the Red hurt animation & fov bobbing
```toml
hurt_animation=true
```
#### Protect Creative
Should players in creative be protected against PVP
```toml
protect_creative=true
```
#### Knockback
Has PVP Knockback (Velocity)
```toml
knockback=true
```
#### Swing
Should player swing when attacking
```toml
swing=true
```
### Logging
`logging`
Whether Logging is enabled
```toml
enable=true
```
#### Level
At which level should be logged
```toml
level=Info
```
```toml
Off
Error
Warn
Info
Debug
Trace
```
#### Env
Enables the user to choose log level by setting `RUST_LOG=<level>` environment variable
```toml
env=false
```
#### Threads
Should threads be printed in the message
```toml
threads=true
```
#### Color
Should color be enabled for logging messages
```toml
color=true
```
#### Timestamp
Should the timestamp be printed in the message
```toml
timestamp=true
```

117
docs/config/basic.md Normal file
View File

@@ -0,0 +1,117 @@
### Basic Configuration
Representing `configuration.toml`
### Server Address
The address to bind the server to
```toml
server_address=0.0.0.0
```
### Seed
The seed for world generation
```toml
seed=
```
### Max players
The maximum number of players allowed on the server
```toml
max_players=10000
```
### View distance
The maximum view distance for players
```toml
view_distance=10
```
### Simulation distance
The maximum simulation distance for players
```toml
simulation_distance=10
```
### Default difficulty
The default game difficulty
```toml
default_difficulty=Normal
```
```toml
Peaceful
Easy
Normal
Hard
```
### Allow nether
Whether the Nether dimension is enabled
```toml
allow_nether=true
```
### Hardcore
Whether the server is in hardcore mode.
```toml
hardcore=true
```
### Online Mode
Whether online mode is enabled. Requires valid Minecraft accounts
```toml
online_mode=true
```
### Encryption
Whether packet encryption is enabled
> [!IMPORTANT]
> Required when online mode is enabled
```toml
encryption=true
```
### Motd
The server's description displayed on the status screen.
```toml
motd=true
```
### Default gamemode
The default game mode for players
```toml
default_gamemode=Survival
```
```toml
Undefined
Survival
Creative
Adventure
Spectator
```

View File

@@ -0,0 +1,16 @@
### Configuration
Pumpkin offers a robust configuration system that allows users to customize various aspects of the server's behavior without relying on external plugins. This provides flexibility and control over the server's operation.
### Basic / Advanced
Pumpkin's Configuration is split into a basic Configuration made for quick changes and important changes and a more Advanced Configuration
- `configuration.toml`: simple and can be compared to the vanilla `server.properties`.
- `features.toml`: designed to have all features of pumpkin at one place, making it a large configuration
#### Key Features:
- Extensive Customization: Configure server settings, player behavior, world generation, and more.
- Performance Optimization: Optimize server performance through configuration tweaks.
- Plugin-Free Customization: Achieve desired changes without the need for additional plugins.

View File

@@ -0,0 +1,73 @@
### Authentication
### Why Authentication
Minecraft is the most Popular game out there, And is is very easy to play it without paying for it. In Fact you don't pay for the Game, You pay for an Minecraft Account.
People who don't bough the Game but play online are using [Cracked Accounts](#cracked-accounts)
#### Cracked Accounts
- Don't cost any Money
- Everyone can set their own Nickname
- Have no UUID
- Have no Skin/Cape
- Not Secure
The Problem is that everyone can name themself how they want, Allowing to Join the Server as a Staff Member for example and having extended permissions,
Cracked accounts are also often used for Botting and [Denial of Service](https://de.wikipedia.org/wiki/Denial_of_Service) Attacks.
### Cracked Server
By default the `online_mode` is enabled in the configuration, This enables Authentication disallowing [Cracked Accounts](#cracked-accounts). When you are willing to allow Cracked Accounts, you can dissable `online_mode`
in the `configuration.toml`
### How Mojang Authentication works
To ensure a player has a premium accounts:
1. A client with a premium account sends a login request to the Mojang session server.
2. **Mojang's servers** verify the client's credentials and add the player to the their Servers
3. Now our server will send a Request to the Session servers and check if the Player has joined the Session Server.
4. If the request was successfull, It will give use more information about the Player (e.g. UUID, Name, Skin/Cape...)
### Custom Authentication Server
Pumpkin does support custom Authentication servers, You can replace the Authentication URL in `features.toml`.
#### How Pumpkin Authentication Works
1. **GET Request:** Pumpkin sends a GET request to the specified authentication URL.
2. **Status Code 200:** If the authentication is successful, the server responds with a status code of 200.
3. **Parse JSON Game Profile:** Pumpkin parses the JSON game profile returned in the response.
#### Game Profile
```rust
struct GameProfile {
id: UUID,
name: String,
properties: Vec<Property>,
profile_actions: Option<Vec<ProfileAction>>, // Optional, Only present when actions are applied
}
```
##### Property
```rust
struct Property {
name: String,
value: String, // Base64 encoded
signature: Option<String>, // Optional, Base64 encoded
}
```
##### Profile Action
```rust
enum ProfileAction {
FORCED_NAME_CHANGE,
USING_BANNED_SKIN,
}
```

View File

@@ -0,0 +1,8 @@
### Introduction
Welcome to the Pumpkin Documentation!
Whether you're an internal Pumpkin developer or working on a Pumpkin plugin, this documentation is your resource for everything Pumpkin.
> [!IMPORTANT]
> While Pumpkin currently doesn't have plugin support yet, this documentation provides valuable insights into the platform's architecture and functionality, which can be helpful for understanding how to create potential future plugins.

View File

@@ -0,0 +1,272 @@
### Networking
Most of the Networking code in Pumpkin, can be found at [Pumpkin-Protocol](https://github.com/Snowiiii/Pumpkin/tree/master/pumpkin-protocol)
Serverbound: Client->Server
Clientbound: Server->Client
### Structure
Packets in the Pumpkin protocol are organized by functionality and state.
`server`: Contains definitions for serverbound packets.
`client`: Contains definitions for clientbound packets.
### States
**Handshake**: Always the first packet being send from the Client. This begins also determins the next state, usally to indicate if the player thans perform a Status Request, Join the Server or wants to be transfered.
**Status**: Indicates the Client wants to see a Status response (MOTD).
**Login**: The Login sequence. Indicates the Client wants to join to the Server
**Config**: A sequence of Configuration packets beining mostly send from the Server to the Client. (Features, Resource Pack, Server Links...)
**Play**: The final state which indicate the Player is now ready to Join in also used to handle all other Gameplay packets.
### Minecraft Protocol
You can find all Minecraft Java packets at https://wiki.vg/Protocol. There you also can see in which [State](#States) they are.
You also can see all the information the Packets has which we can either Write or Read depending if its Serverbound or Clientbound
### Adding a Clientbound Packet
1. Adding a Packet is easy. First you have to dereive serde Serialize for packets.
```rust
#[derive(Serialize)]
```
2. Next you have set the packet id using the packet macro
```rust
#[packet(0x1D)]
```
3. Now you can create the Struct.
> [!IMPORTANT]
> Please start the Packet name with "C" for Clientbound.
> Also please add the State to the packet if its a Packet sended in multiple States, For example there are 3 Disconnect Packets.
>
> - CLoginDisconnect
> - CConfigDisconnect
> - CPlayDisconnect
Create fields within your packet structure to represent the data that will be sent to the client.
> [!IMPORTANT]
> Use descriptive field names and appropriate data types.
Example:
```rust
pub struct CPlayDisconnect {
reason: TextComponent,
more fields...
}
```
4. Also don't forgot to impl a new function for Clientbound Packets so we can actaully send then by putting in the values
Example:
```rust
impl CPlayDisconnect {
pub fn new(reason: TextComponent) -> Self {
Self { reason }
}
}
```
5. At the End everything should come together,
```rust
#[derive(Serialize)]
#[packet(0x1D)]
pub struct CPlayDisconnect {
reason: TextComponent,
}
impl CPlayDisconnect {
pub fn new(reason: TextComponent) -> Self {
Self { reason }
}
}
```
6. You can also Serialize the Packet manually, Which can be usefull if the Packet is more complex
```diff
-#[derive(Serialize)]
+ impl ClientPacket for CPlayDisconnect {
+ fn write(&self, bytebuf: &mut crate::bytebuf::ByteBuffer) {
+ bytebuf.put_slice(&self.reason.encode());
+ }
```
7. You can now send the Packet. See [Sending Packets](#sending-packets)
### Adding a Serverbound Packet
1. Adding a Packet is easy. First you have to dereive serde Deserialize for packets.
```rust
#[derive(Deserialize)]
```
2. Next you have set the packet id using the packet macro
```rust
#[packet(0x1A)]
```
3. Now you can create the Struct.
> [!IMPORTANT]
> Please start the Packet name with "S" for Serverbound.
> Also please add the State to the packet if its a Packet sended in multiple States.
Create fields within your packet structure to represent the data that will be sent to the client.
> [!IMPORTANT]
> Use descriptive field names and appropriate data types.
Example:
```rust
pub struct SPlayerPosition {
pub x: f64,
pub feet_y: f64,
pub z: f64,
pub ground: bool,
}
```
4. At the End everything should come together,
```rust
#[derive(Deserialize)]
#[packet(0x1A)]
pub struct SPlayerPosition {
pub x: f64,
pub feet_y: f64,
pub z: f64,
pub ground: bool,
}
```
5. You can also Deserialize the Packet manually, Which can be usefull if the Packet is more complex
```diff
-#[derive(Deserialize)]
+ impl ServerPacket for SPlayerPosition {
+ fn read(bytebuf: &mut ByteBuffer) -> Result<Self, DeserializerError> {
+ Ok(Self {
+ x: bytebuf.get_f64()?,
+ feet_y: bytebuf.get_f64()?,
+ z: bytebuf.get_f64()?,
+ ground: bytebuf.get_bool()?,
+ })
+ }
```
6. You can listen for the Packet. See [Receive Packets](#receiving-packets)
### Client
Pumpkin has stores Client and Players seperatly, Everything what is not reached the Play State is a Simple Client. Here are the Differences
**Client**
- Can only be in Status/Login/Transfer/Config State
- Is not a living entity
- Has small resource consumption
**Player**
- Can only be in Play State
- Is a living entity in a world
- Has more data, Consumes more resources
#### Sending Packets
Example:
```rust
// Works only in Status State
client.send_packet(&CStatusResponse::new("{ description: "A Description"}"));
```
#### Receiving Packets
For Clients:
`src/client/mod.rs`
```diff
// Put the Packet into the right State
fn handle_mystate_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SHandShake::PACKET_ID => {
self.handle_handshake(server, SHandShake::read(bytebuf)?);
Ok(())
}
+ MyPacket::PACKET_ID => {
+ self.handle_mypacket(server, MyPacket::read(bytebuf)?);
+ Ok(())
+ }
_ => {
log::error!(
"Failed to handle packet id {} while in ... state",
packet.id.0
);
Ok(())
}
}
}
```
For Players:
`src/entity/player.rs`
```diff
// Players only have Play State
fn handle_play_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SHandShake::PACKET_ID => {
self.handle_handshake(server, SHandShake::read(bytebuf)?);
Ok(())
}
+ MyPacket::PACKET_ID => {
+ self.handle_mypacket(server, MyPacket::read(bytebuf)?);
+ Ok(())
+ }
_ => {
log::error!(
"Failed to handle packet id {} while in ... state",
packet.id.0
);
Ok(())
}
}
}
```
### Porting
To port to a new Minecraft version, You can compare difference in Protocol on wiki.vg https://wiki.vg/index.php?title=Protocol&action=history
Also change the `CURRENT_MC_PROTOCOL` in `src/lib.rs`

View File

@@ -11,11 +11,11 @@ hero:
text: Quick Start
link: /about/quick-start
- theme: alt
text: Documentation
link: /about/introduction
text: Configuration
link: /config/introduction
- theme: alt
text: For developers
link: /plugins/about
link: /developer/introduction
features:
- title: Written in Rust

View File

@@ -1,15 +0,0 @@
# Plugins
Pumpkin uses [Extism](https://extism.org/) for loading plugins.
This means that you can write your plugins in any language that can compile to Extism WASM.
These languages include:
- Rust
- JavaScript / TypeScript
- Golang
- C#
- F#
- C
- Haskell
- Zig
- AssemblyScript

View File

@@ -1,4 +0,0 @@
# Getting Started in Rust
Rust in one of the supported plugin languages.
This page has not been written yet.

View File

@@ -0,0 +1,33 @@
### Common Issues
1. ### Broken Chunk Lighting
See [#93](https://github.com/Snowiiii/Pumpkin/issues/93)
**Issue:** Broken chunk lighting in your Minecraft server.
**Cause:** The server is currently not calculating lighting for chunks, we working on that.
**Temporary Fix:** Use a full-bright resource pack. This will temporarily resolve the issue by making all chunks appear brightly lit. You can find many full-bright resource packs online.
2. ### I can place blocks inside me
See [#49](https://github.com/Snowiiii/Pumpkin/issues/49)
**Issue:** Players are able to place block in them.
**Cause:** The server is currently not calculating hitboxes for blocks, we working on that.
3. ### Server is unresponsive
**Issue:** You have to wait before reconnect or can't do basic things while chunks are loading.
**Cause:** The server has currently blocking issues, we working on that.
4. ### Failed to verify username
**Issue:** Some players reported having issues loggin into the Server, Having "Failed to verify username" error.
**Cause:** This has to do with Authentication, Usally with the prevent proxy connections setting.
**Fix:** Disable `prevent_proxy_connections` in `features.toml`

1765
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@
"docs:preview": "vitepress preview docs"
},
"devDependencies": {
"vitepress": "^1.3.4",
"vue": "^3.4.38"
"vitepress": "^1.4.1",
"vue": "^3.5.12"
}
}

View File

@@ -9,4 +9,4 @@ serde.workspace = true
log.workspace = true
toml = "0.8"
serde-inline-default = "0.2.1"

View File

@@ -1,50 +1,84 @@
use pumpkin_core::ProfileAction;
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
#[derive(Deserialize, Serialize)]
pub struct AuthenticationConfig {
/// Whether to use Mojang authentication.
#[serde_inline_default(true)]
pub enabled: bool,
pub auth_url: String,
/// Prevent proxy connections.
#[serde_inline_default(false)]
pub prevent_proxy_connections: bool,
pub prevent_proxy_connection_auth_url: String,
/// Player profile handling.
#[serde(default)]
pub player_profile: PlayerProfileConfig,
/// Texture handling.
#[serde(default)]
pub textures: TextureConfig,
}
impl Default for AuthenticationConfig {
fn default() -> Self {
Self {
enabled: true,
prevent_proxy_connections: false,
player_profile: Default::default(),
textures: Default::default(),
auth_url: "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}".to_string(),
prevent_proxy_connection_auth_url: "https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}".to_string(),
}
}
}
#[derive(Deserialize, Serialize)]
#[serde(default)]
pub struct PlayerProfileConfig {
/// Allow players flagged by Mojang (banned, forced name change).
pub allow_banned_players: bool,
/// Depends on the value above
#[serde(default = "default_allowed_actions")]
pub allowed_actions: Vec<ProfileAction>,
}
fn default_allowed_actions() -> Vec<ProfileAction> {
vec![
ProfileAction::ForcedNameChange,
ProfileAction::UsingBannedSkin,
]
}
impl Default for PlayerProfileConfig {
fn default() -> Self {
Self {
allow_banned_players: false,
allowed_actions: vec![
ProfileAction::ForcedNameChange,
ProfileAction::UsingBannedSkin,
],
allowed_actions: default_allowed_actions(),
}
}
}
#[serde_inline_default]
#[derive(Deserialize, Serialize)]
pub struct TextureConfig {
/// Whether to use player textures.
#[serde_inline_default(true)]
pub enabled: bool,
#[serde_inline_default(vec!["http".into(), "https".into()])]
pub allowed_url_schemes: Vec<String>,
#[serde_inline_default(vec![".minecraft.net".into(), ".mojang.com".into()])]
pub allowed_url_domains: Vec<String>,
/// Specific texture types.
#[serde(default)]
pub types: TextureTypes,
}
@@ -60,13 +94,17 @@ impl Default for TextureConfig {
}
#[derive(Deserialize, Serialize)]
#[serde_inline_default]
pub struct TextureTypes {
/// Use player skins.
#[serde_inline_default(true)]
pub skin: bool,
/// Use player capes.
#[serde_inline_default(true)]
pub cape: bool,
/// Use player elytras.
/// (i didn't know myself that there are custom elytras)
#[serde_inline_default(true)]
pub elytra: bool,
}
@@ -79,14 +117,3 @@ impl Default for TextureTypes {
}
}
}
impl Default for AuthenticationConfig {
fn default() -> Self {
Self {
enabled: true,
prevent_proxy_connections: false,
player_profile: Default::default(),
textures: Default::default(),
}
}
}

View File

@@ -1,14 +1,22 @@
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[derive(Deserialize, Serialize)]
#[serde_inline_default]
pub struct CommandsConfig {
/// Are commands from the Console accepted ?
#[serde_inline_default(true)]
pub use_console: bool,
// TODO: commands...
/// Should be commands from players be logged in console?
#[serde_inline_default(true)]
pub log_console: bool, // TODO: commands...
}
impl Default for CommandsConfig {
fn default() -> Self {
Self { use_console: true }
Self {
use_console: true,
log_console: true,
}
}
}

View File

@@ -1,24 +1,46 @@
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
#[derive(Deserialize, Serialize)]
// Packet compression
/// Packet compression
pub struct CompressionConfig {
/// Is compression enabled ?
/// Wether compression is enabled
#[serde_inline_default(true)]
pub enabled: bool,
#[serde(flatten)]
#[serde(default)]
pub compression_info: CompressionInfo,
}
#[serde_inline_default]
#[derive(Deserialize, Serialize, Clone)]
/// We have this in a Seperate struct so we can use it outside of the Config
pub struct CompressionInfo {
/// The compression threshold used when compression is enabled
pub compression_threshold: u32,
#[serde_inline_default(256)]
pub threshold: u32,
/// A value between 0..9
/// 1 = Optimize for the best speed of encoding.
/// 9 = Optimize for the size of data being encoded.
pub compression_level: u32,
#[serde_inline_default(4)]
pub level: u32,
}
impl Default for CompressionInfo {
fn default() -> Self {
Self {
threshold: 256,
level: 4,
}
}
}
impl Default for CompressionConfig {
fn default() -> Self {
Self {
enabled: true,
compression_threshold: 256,
compression_level: 4,
compression_info: Default::default(),
}
}
}

View File

@@ -1,7 +1,11 @@
use log::warn;
use logging::LoggingConfig;
use pumpkin_core::{Difficulty, GameMode};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
// TODO: when https://github.com/rust-lang/rfcs/pull/3681 gets merged, replace serde-inline-default with native syntax
use serde_inline_default::serde_inline_default;
use std::{
fs,
net::{Ipv4Addr, SocketAddr},
@@ -10,6 +14,7 @@ use std::{
};
pub mod auth;
pub mod logging;
pub mod proxy;
pub mod resource_pack;
@@ -20,16 +25,13 @@ pub use pvp::PVPConfig;
pub use rcon::RCONConfig;
mod commands;
mod compression;
pub mod compression;
mod pvp;
mod rcon;
use proxy::ProxyConfig;
use resource_pack::ResourcePackConfig;
/// Current Config version of the Base Config
const CURRENT_BASE_VERSION: &str = "1.0.0";
pub static ADVANCED_CONFIG: LazyLock<AdvancedConfiguration> =
LazyLock::new(AdvancedConfiguration::load);
@@ -41,6 +43,7 @@ pub static BASIC_CONFIG: LazyLock<BasicConfiguration> = LazyLock::new(BasicConfi
/// This also allows you get some Performance or Resource boosts.
/// Important: The Configuration should match Vanilla by default
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct AdvancedConfiguration {
pub proxy: ProxyConfig,
pub authentication: AuthenticationConfig,
@@ -49,43 +52,58 @@ pub struct AdvancedConfiguration {
pub commands: CommandsConfig,
pub rcon: RCONConfig,
pub pvp: PVPConfig,
pub logging: LoggingConfig,
}
#[serde_inline_default]
#[derive(Serialize, Deserialize)]
pub struct BasicConfiguration {
/// A version identifier for the configuration format.
pub config_version: String,
/// The address to bind the server to.
#[serde(default = "default_server_address")]
pub server_address: SocketAddr,
/// The seed for world generation.
#[serde(default = "String::new")]
pub seed: String,
/// The maximum number of players allowed on the server.
#[serde_inline_default(10000)]
pub max_players: u32,
/// The maximum view distance for players.
#[serde_inline_default(10)]
pub view_distance: u8,
/// The maximum simulated view distance.
#[serde_inline_default(10)]
pub simulation_distance: u8,
/// The default game difficulty.
#[serde_inline_default(Difficulty::Normal)]
pub default_difficulty: Difficulty,
/// Whether the Nether dimension is enabled.
#[serde_inline_default(true)]
pub allow_nether: bool,
/// Whether the server is in hardcore mode.
#[serde_inline_default(false)]
pub hardcore: bool,
/// Whether online mode is enabled. Requires valid Minecraft accounts.
#[serde_inline_default(true)]
pub online_mode: bool,
/// Whether packet encryption is enabled. Required when online mode is enabled.
#[serde_inline_default(true)]
pub encryption: bool,
/// The server's description displayed on the status screen.
#[serde_inline_default("A Blazing fast Pumpkin Server!".to_string())]
pub motd: String,
/// The default game mode for players.
#[serde_inline_default(GameMode::Survival)]
pub default_gamemode: GameMode,
}
fn default_server_address() -> SocketAddr {
SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25565)
}
impl Default for BasicConfiguration {
fn default() -> Self {
Self {
config_version: CURRENT_BASE_VERSION.to_string(),
server_address: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25565),
server_address: default_server_address(),
seed: "".to_string(),
max_players: 100000,
view_distance: 10,
@@ -114,7 +132,7 @@ trait LoadConfiguration {
toml::from_str(&file_content).unwrap_or_else(|err| {
panic!(
"Couldn't parse config at {:?}. Reason: {}",
"Couldn't parse config at {:?}. Reason: {}. This is is proberbly caused by an Config update, Just delete the old Config and start Pumpkin again",
path,
err.message()
)
@@ -124,7 +142,7 @@ trait LoadConfiguration {
if let Err(err) = fs::write(path, toml::to_string(&content).unwrap()) {
warn!(
"Couldn't write default config to {:?}. Reason: {}",
"Couldn't write default config to {:?}. Reason: {}. This is is proberbly caused by an Config update, Just delete the old Config and start Pumpkin again",
path, err
);
}
@@ -157,10 +175,6 @@ impl LoadConfiguration for BasicConfiguration {
}
fn validate(&self) {
assert_eq!(
self.config_version, CURRENT_BASE_VERSION,
"Config version does not match used Config version. Please update your config"
);
assert!(self.view_distance >= 2, "View distance must be at least 2");
assert!(
self.view_distance <= 32,

View File

@@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
#[derive(Deserialize, Serialize)]
pub struct LoggingConfig {
#[serde_inline_default(true)]
pub enabled: bool,
#[serde_inline_default(LevelFilter::Info)]
pub level: LevelFilter,
#[serde_inline_default(false)]
pub env: bool,
#[serde_inline_default(true)]
pub threads: bool,
#[serde_inline_default(true)]
pub color: bool,
#[serde_inline_default(true)]
pub timestamp: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
enabled: true,
level: LevelFilter::Info,
env: false,
threads: true,
color: true,
timestamp: true,
}
}
}
#[derive(Deserialize, Serialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub enum LevelFilter {
/// A level lower than all log levels.
Off,
/// Corresponds to the `Error` log level.
Error,
/// Corresponds to the `Warn` log level.
Warn,
/// Corresponds to the `Info` log level.
Info,
/// Corresponds to the `Debug` log level.
Debug,
/// Corresponds to the `Trace` log level.
Trace,
}

View File

@@ -1,12 +1,14 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ProxyConfig {
pub enabled: bool,
pub velocity: VelocityConfig,
}
#[derive(Deserialize, Serialize)]
#[serde(default)]
pub struct VelocityConfig {
pub enabled: bool,
pub secret: String,

View File

@@ -1,16 +1,23 @@
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
#[derive(Deserialize, Serialize)]
pub struct PVPConfig {
/// Is PVP enabled ?
#[serde_inline_default(true)]
pub enabled: bool,
/// Do we want to have the Red hurt animation & fov bobbing
#[serde_inline_default(true)]
pub hurt_animation: bool,
/// Should players in creative be protected against PVP
#[serde_inline_default(true)]
pub protect_creative: bool,
/// Has PVP Knockback?
#[serde_inline_default(true)]
pub knockback: bool,
/// Should player swing when attacking?
#[serde_inline_default(true)]
pub swing: bool,
}

View File

@@ -1,20 +1,68 @@
use std::net::{Ipv4Addr, SocketAddr};
use serde::{Deserialize, Serialize};
use serde_inline_default::serde_inline_default;
#[serde_inline_default]
#[derive(Deserialize, Serialize, Clone)]
pub struct RCONConfig {
/// Is RCON Enabled?
#[serde_inline_default(false)]
pub enabled: bool,
/// The network address and port where the RCON server will listen for connections.
#[serde(default = "default_rcon_address")]
pub address: SocketAddr,
/// The password required for RCON authentication.
#[serde(default)]
pub password: String,
/// The maximum number of concurrent RCON connections allowed.
/// If 0 there is no limit
#[serde(default)]
pub max_connections: u32,
/// RCON Logging
pub logging: RCONLogging,
}
#[serde_inline_default]
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct RCONLogging {
/// Whether successful RCON logins should be logged.
#[serde_inline_default(true)]
pub log_logged_successfully: bool,
/// Whether failed RCON login attempts with incorrect passwords should be logged.
#[serde_inline_default(true)]
pub log_wrong_password: bool,
/// Whether all RCON commands, regardless of success or failure, should be logged.
#[serde_inline_default(true)]
pub log_commands: bool,
/// Whether RCON quit commands should be logged.
#[serde_inline_default(true)]
pub log_quit: bool,
}
impl Default for RCONLogging {
fn default() -> Self {
Self {
log_logged_successfully: true,
log_wrong_password: true,
log_commands: true,
log_quit: true,
}
}
}
fn default_rcon_address() -> SocketAddr {
SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25575)
}
impl Default for RCONConfig {
fn default() -> Self {
Self {
enabled: false,
address: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 25575),
address: default_rcon_address(),
password: "".to_string(),
max_connections: 0,
logging: Default::default(),
}
}
}

View File

@@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
#[serde(default)]
pub struct ResourcePackConfig {
pub enabled: bool,
/// The path to the resource pack.

View File

@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
use super::vector3::Vector3;
#[derive(Clone, Copy)]
/// Aka Block Position
pub struct WorldPosition(pub Vector3<i32>);

View File

@@ -93,12 +93,14 @@ impl<T: Math + Copy> Neg for Vector3<T> {
}
impl<T> From<(T, T, T)> for Vector3<T> {
#[inline(always)]
fn from((x, y, z): (T, T, T)) -> Self {
Vector3 { x, y, z }
}
}
impl<T> From<Vector3<T>> for (T, T, T) {
#[inline(always)]
fn from(vector: Vector3<T>) -> Self {
(vector.x, vector.y, vector.z)
}

View File

@@ -1,18 +1,14 @@
use super::Random;
use super::RandomImpl;
pub trait GaussianGenerator: Random {
fn has_next_gaussian(&self) -> bool;
pub trait GaussianGenerator: RandomImpl {
fn stored_next_gaussian(&self) -> Option<f64>;
fn set_has_next_gaussian(&mut self, value: bool);
fn stored_next_gaussian(&self) -> f64;
fn set_stored_next_gaussian(&mut self, value: f64);
fn set_stored_next_gaussian(&mut self, value: Option<f64>);
fn calculate_gaussian(&mut self) -> f64 {
if self.has_next_gaussian() {
self.set_has_next_gaussian(false);
self.stored_next_gaussian()
if let Some(gaussian) = self.stored_next_gaussian() {
self.set_stored_next_gaussian(None);
gaussian
} else {
loop {
let d = 2f64 * self.next_f64() - 1f64;
@@ -21,8 +17,7 @@ pub trait GaussianGenerator: Random {
if f < 1f64 && f != 0f64 {
let g = (-2f64 * f.ln() / f).sqrt();
self.set_stored_next_gaussian(e * g);
self.set_has_next_gaussian(true);
self.set_stored_next_gaussian(Some(e * g));
return d * g;
}
}

View File

@@ -1,11 +1,10 @@
use super::{
gaussian::GaussianGenerator, hash_block_pos, java_string_hash, Random, RandomSplitter,
gaussian::GaussianGenerator, hash_block_pos, java_string_hash, RandomDeriverImpl, RandomImpl,
};
struct LegacyRand {
pub struct LegacyRand {
seed: u64,
internal_next_gaussian: f64,
internal_has_next_gaussian: bool,
internal_next_gaussian: Option<f64>,
}
impl LegacyRand {
@@ -18,29 +17,20 @@ impl LegacyRand {
}
impl GaussianGenerator for LegacyRand {
fn has_next_gaussian(&self) -> bool {
self.internal_has_next_gaussian
}
fn stored_next_gaussian(&self) -> f64 {
fn stored_next_gaussian(&self) -> Option<f64> {
self.internal_next_gaussian
}
fn set_has_next_gaussian(&mut self, value: bool) {
self.internal_has_next_gaussian = value;
}
fn set_stored_next_gaussian(&mut self, value: f64) {
fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
self.internal_next_gaussian = value;
}
}
impl Random for LegacyRand {
impl RandomImpl for LegacyRand {
fn from_seed(seed: u64) -> Self {
LegacyRand {
seed: (seed ^ 0x5DEECE66D) & 0xFFFFFFFFFFFF,
internal_has_next_gaussian: false,
internal_next_gaussian: 0f64,
internal_next_gaussian: None,
}
}
@@ -77,7 +67,8 @@ impl Random for LegacyRand {
self.next(1) != 0
}
fn next_splitter(&mut self) -> impl RandomSplitter {
#[allow(refining_impl_trait)]
fn next_splitter(&mut self) -> LegacySplitter {
LegacySplitter::new(self.next_i64() as u64)
}
@@ -86,13 +77,13 @@ impl Random for LegacyRand {
}
fn next_bounded_i32(&mut self, bound: i32) -> i32 {
if bound & (bound - 1) == 0 {
(bound as u64).wrapping_mul(self.next(31) >> 31) as i32
if (bound & bound.wrapping_sub(1)) == 0 {
((bound as u64).wrapping_mul(self.next(31)) >> 31) as i32
} else {
loop {
let i = self.next(31) as i32;
let j = i % bound;
if (i - j + (bound - 1)) > 0 {
if (i.wrapping_sub(j).wrapping_add(bound.wrapping_sub(1))) >= 0 {
return j;
}
}
@@ -100,7 +91,7 @@ impl Random for LegacyRand {
}
}
struct LegacySplitter {
pub struct LegacySplitter {
seed: u64,
}
@@ -110,17 +101,18 @@ impl LegacySplitter {
}
}
impl RandomSplitter for LegacySplitter {
fn split_u64(&self, seed: u64) -> impl Random {
#[allow(refining_impl_trait)]
impl RandomDeriverImpl for LegacySplitter {
fn split_u64(&self, seed: u64) -> LegacyRand {
LegacyRand::from_seed(seed)
}
fn split_string(&self, seed: &str) -> impl Random {
fn split_string(&self, seed: &str) -> LegacyRand {
let string_hash = java_string_hash(seed);
LegacyRand::from_seed((string_hash as u64) ^ self.seed)
}
fn split_pos(&self, x: i32, y: i32, z: i32) -> impl Random {
fn split_pos(&self, x: i32, y: i32, z: i32) -> LegacyRand {
let pos_hash = hash_block_pos(x, y, z);
LegacyRand::from_seed((pos_hash as u64) ^ self.seed)
}
@@ -128,7 +120,7 @@ impl RandomSplitter for LegacySplitter {
#[cfg(test)]
mod test {
use crate::random::{Random, RandomSplitter};
use crate::random::{RandomDeriverImpl, RandomImpl};
use super::LegacyRand;
@@ -163,6 +155,17 @@ mod test {
for value in values {
assert_eq!(rand.next_bounded_i32(0xf), value);
}
let mut rand = LegacyRand::from_seed(0);
for _ in 0..10 {
assert_eq!(rand.next_bounded_i32(1), 0);
}
let mut rand = LegacyRand::from_seed(0);
let values = [1, 1, 0, 1, 1, 0, 1, 0, 1, 1];
for value in values {
assert_eq!(rand.next_bounded_i32(2), value);
}
}
#[test]

View File

@@ -1,13 +1,156 @@
use legacy_rand::{LegacyRand, LegacySplitter};
use xoroshiro128::{Xoroshiro, XoroshiroSplitter};
mod gaussian;
pub mod legacy_rand;
pub mod xoroshiro128;
pub trait Random {
pub enum RandomGenerator {
Xoroshiro(Xoroshiro),
Legacy(LegacyRand),
}
impl RandomGenerator {
#[inline]
pub fn split(&mut self) -> Self {
match self {
Self::Xoroshiro(rand) => Self::Xoroshiro(rand.split()),
Self::Legacy(rand) => Self::Legacy(rand.split()),
}
}
#[inline]
pub fn next_splitter(&mut self) -> RandomDeriver {
match self {
Self::Xoroshiro(rand) => RandomDeriver::Xoroshiro(rand.next_splitter()),
Self::Legacy(rand) => RandomDeriver::Legacy(rand.next_splitter()),
}
}
#[inline]
pub fn next(&mut self, bits: u64) -> u64 {
match self {
Self::Xoroshiro(rand) => rand.next(bits),
Self::Legacy(rand) => rand.next(bits),
}
}
#[inline]
pub fn next_i32(&mut self) -> i32 {
match self {
Self::Xoroshiro(rand) => rand.next_i32(),
Self::Legacy(rand) => rand.next_i32(),
}
}
#[inline]
pub fn next_bounded_i32(&mut self, bound: i32) -> i32 {
match self {
Self::Xoroshiro(rand) => rand.next_bounded_i32(bound),
Self::Legacy(rand) => rand.next_bounded_i32(bound),
}
}
#[inline]
pub fn next_inbetween_i32(&mut self, min: i32, max: i32) -> i32 {
self.next_bounded_i32(max - min + 1) + min
}
#[inline]
pub fn next_i64(&mut self) -> i64 {
match self {
Self::Xoroshiro(rand) => rand.next_i64(),
Self::Legacy(rand) => rand.next_i64(),
}
}
#[inline]
pub fn next_bool(&mut self) -> bool {
match self {
Self::Xoroshiro(rand) => rand.next_bool(),
Self::Legacy(rand) => rand.next_bool(),
}
}
#[inline]
pub fn next_f32(&mut self) -> f32 {
match self {
Self::Xoroshiro(rand) => rand.next_f32(),
Self::Legacy(rand) => rand.next_f32(),
}
}
#[inline]
pub fn next_f64(&mut self) -> f64 {
match self {
Self::Xoroshiro(rand) => rand.next_f64(),
Self::Legacy(rand) => rand.next_f64(),
}
}
#[inline]
pub fn next_gaussian(&mut self) -> f64 {
match self {
Self::Xoroshiro(rand) => rand.next_gaussian(),
Self::Legacy(rand) => rand.next_gaussian(),
}
}
#[inline]
pub fn next_triangular(&mut self, mode: f64, deviation: f64) -> f64 {
mode + deviation * (self.next_f64() - self.next_f64())
}
#[inline]
pub fn skip(&mut self, count: i32) {
for _ in 0..count {
self.next_i64();
}
}
#[inline]
pub fn next_inbetween_i32_exclusive(&mut self, min: i32, max: i32) -> i32 {
min + self.next_bounded_i32(max - min)
}
}
pub enum RandomDeriver {
Xoroshiro(XoroshiroSplitter),
Legacy(LegacySplitter),
}
impl RandomDeriver {
#[inline]
pub fn split_string(&self, seed: &str) -> RandomGenerator {
match self {
Self::Xoroshiro(deriver) => RandomGenerator::Xoroshiro(deriver.split_string(seed)),
Self::Legacy(deriver) => RandomGenerator::Legacy(deriver.split_string(seed)),
}
}
#[inline]
pub fn split_u64(&self, seed: u64) -> RandomGenerator {
match self {
Self::Xoroshiro(deriver) => RandomGenerator::Xoroshiro(deriver.split_u64(seed)),
Self::Legacy(deriver) => RandomGenerator::Legacy(deriver.split_u64(seed)),
}
}
#[inline]
pub fn split_pos(&self, x: i32, y: i32, z: i32) -> RandomGenerator {
match self {
Self::Xoroshiro(deriver) => RandomGenerator::Xoroshiro(deriver.split_pos(x, y, z)),
Self::Legacy(deriver) => RandomGenerator::Legacy(deriver.split_pos(x, y, z)),
}
}
}
pub trait RandomImpl {
fn from_seed(seed: u64) -> Self;
fn split(&mut self) -> Self;
fn next_splitter(&mut self) -> impl RandomSplitter;
fn next_splitter(&mut self) -> impl RandomDeriverImpl;
fn next(&mut self, bits: u64) -> u64;
@@ -44,12 +187,12 @@ pub trait Random {
}
}
pub trait RandomSplitter {
fn split_string(&self, seed: &str) -> impl Random;
pub trait RandomDeriverImpl {
fn split_string(&self, seed: &str) -> impl RandomImpl;
fn split_u64(&self, seed: u64) -> impl Random;
fn split_u64(&self, seed: u64) -> impl RandomImpl;
fn split_pos(&self, x: i32, y: i32, z: i32) -> impl Random;
fn split_pos(&self, x: i32, y: i32, z: i32) -> impl RandomImpl;
}
fn hash_block_pos(x: i32, y: i32, z: i32) -> i64 {

View File

@@ -1,10 +1,9 @@
use super::{gaussian::GaussianGenerator, hash_block_pos, Random, RandomSplitter};
use super::{gaussian::GaussianGenerator, hash_block_pos, RandomDeriverImpl, RandomImpl};
pub struct Xoroshiro {
lo: u64,
hi: u64,
internal_next_gaussian: f64,
internal_has_next_gaussian: bool,
internal_next_gaussian: Option<f64>,
}
impl Xoroshiro {
@@ -17,8 +16,7 @@ impl Xoroshiro {
Self {
lo,
hi,
internal_next_gaussian: 0f64,
internal_has_next_gaussian: false,
internal_next_gaussian: None,
}
}
@@ -45,21 +43,13 @@ impl Xoroshiro {
}
impl GaussianGenerator for Xoroshiro {
fn stored_next_gaussian(&self) -> f64 {
fn stored_next_gaussian(&self) -> Option<f64> {
self.internal_next_gaussian
}
fn has_next_gaussian(&self) -> bool {
self.internal_has_next_gaussian
}
fn set_stored_next_gaussian(&mut self, value: f64) {
fn set_stored_next_gaussian(&mut self, value: Option<f64>) {
self.internal_next_gaussian = value;
}
fn set_has_next_gaussian(&mut self, value: bool) {
self.internal_has_next_gaussian = value;
}
}
fn mix_stafford_13(z: u64) -> u64 {
@@ -68,7 +58,7 @@ fn mix_stafford_13(z: u64) -> u64 {
z ^ (z >> 31)
}
impl Random for Xoroshiro {
impl RandomImpl for Xoroshiro {
fn from_seed(seed: u64) -> Self {
let (lo, hi) = Self::mix_u64(seed);
let lo = mix_stafford_13(lo);
@@ -84,7 +74,8 @@ impl Random for Xoroshiro {
self.next_random() >> (64 - bits)
}
fn next_splitter(&mut self) -> impl RandomSplitter {
#[allow(refining_impl_trait)]
fn next_splitter(&mut self) -> XoroshiroSplitter {
XoroshiroSplitter {
lo: self.next_random(),
hi: self.next_random(),
@@ -137,18 +128,19 @@ pub struct XoroshiroSplitter {
hi: u64,
}
impl RandomSplitter for XoroshiroSplitter {
fn split_pos(&self, x: i32, y: i32, z: i32) -> impl Random {
#[allow(refining_impl_trait)]
impl RandomDeriverImpl for XoroshiroSplitter {
fn split_pos(&self, x: i32, y: i32, z: i32) -> Xoroshiro {
let l = hash_block_pos(x, y, z) as u64;
let m = l ^ self.lo;
Xoroshiro::new(m, self.hi)
}
fn split_u64(&self, seed: u64) -> impl Random {
fn split_u64(&self, seed: u64) -> Xoroshiro {
Xoroshiro::new(seed ^ self.lo, seed ^ self.hi)
}
fn split_string(&self, seed: &str) -> impl Random {
fn split_string(&self, seed: &str) -> Xoroshiro {
let bytes = md5::compute(seed.as_bytes());
let l = u64::from_be_bytes(bytes[0..8].try_into().expect("incorrect length"));
let m = u64::from_be_bytes(bytes[8..16].try_into().expect("incorrect length"));
@@ -159,7 +151,7 @@ impl RandomSplitter for XoroshiroSplitter {
#[cfg(test)]
mod tests {
use crate::random::{Random, RandomSplitter};
use crate::random::{RandomDeriverImpl, RandomImpl};
use super::{mix_stafford_13, Xoroshiro};

View File

@@ -5,9 +5,11 @@ edition.workspace = true
[dependencies]
# For items
pumpkin-world = { path = "../pumpkin-world"}
pumpkin-world = { path = "../pumpkin-world" }
num-traits = "0.2"
num-derive = "0.4"
thiserror = "1.0.63"
itertools = "0.13.0"
itertools.workspace = true
parking_lot.workspace = true
crossbeam.workspace = true

View File

@@ -2,9 +2,10 @@ use crate::container_click::MouseDragType;
use crate::{Container, InventoryError};
use itertools::Itertools;
use num_traits::Euclid;
use parking_lot::{Mutex, RwLock};
use pumpkin_world::item::ItemStack;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::Arc;
#[derive(Debug, Default)]
pub struct DragHandler(RwLock<HashMap<u64, Arc<Mutex<Drag>>>>);
@@ -23,10 +24,7 @@ impl DragHandler {
drag_type,
slots: vec![],
};
let mut drags = match self.0.write() {
Ok(drags) => drags,
Err(_) => Err(InventoryError::LockError)?,
};
let mut drags = self.0.write();
drags.insert(container_id, Arc::new(Mutex::new(drag)));
Ok(())
}
@@ -37,13 +35,10 @@ impl DragHandler {
player: i32,
slot: usize,
) -> Result<(), InventoryError> {
let drags = match self.0.read() {
Ok(drags) => drags,
Err(_) => Err(InventoryError::LockError)?,
};
let drags = self.0.read();
match drags.get(&container_id) {
Some(drag) => {
let mut drag = drag.lock().unwrap();
let mut drag = drag.lock();
if drag.player != player {
Err(InventoryError::MultiplePlayersDragging)?
}
@@ -68,13 +63,11 @@ impl DragHandler {
return Ok(());
}
let Ok(mut drags) = self.0.write() else {
Err(InventoryError::LockError)?
};
let mut drags = self.0.write();
let Some((_, drag)) = drags.remove_entry(container_id) else {
Err(InventoryError::OutOfOrderDragging)?
};
let drag = drag.lock().unwrap();
let drag = drag.lock();
if player != drag.player {
Err(InventoryError::MultiplePlayersDragging)?

View File

@@ -1,6 +1,7 @@
use crate::{Container, WindowType};
use parking_lot::Mutex;
use pumpkin_world::item::ItemStack;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
pub struct OpenContainer {
players: Vec<i32>,

View File

@@ -1,3 +1,5 @@
use std::sync::atomic::AtomicU32;
use crate::container_click::MouseClick;
use crate::{handle_item_change, Container, InventoryError, WindowType};
use pumpkin_world::item::ItemStack;
@@ -11,7 +13,7 @@ pub struct PlayerInventory {
offhand: Option<ItemStack>,
// current selected slot in hotbar
selected: usize,
pub state_id: u32,
pub state_id: AtomicU32,
// Notchian server wraps this value at 100, we can just keep it as a u8 that automatically wraps
pub total_opened_containers: u8,
}
@@ -32,7 +34,7 @@ impl PlayerInventory {
offhand: None,
// TODO: What when player spawns in with an different index ?
selected: 0,
state_id: 0,
state_id: AtomicU32::new(0),
total_opened_containers: 2,
}
}

View File

@@ -10,3 +10,6 @@ proc-macro = true
proc-macro2 = "1.0"
quote = "1.0"
syn = "2.0"
serde.workspace = true
itertools.workspace = true
serde_json = "1.0.128"

View File

@@ -0,0 +1,261 @@
use std::{
collections::{HashMap, HashSet},
sync::LazyLock,
};
use itertools::Itertools;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
struct RegistryBlockDefinition {
/// e.g. minecraft:door or minecraft:button
#[serde(rename = "type")]
pub category: String,
/// Specifies the variant of the blocks category.
/// e.g. minecraft:iron_door has the variant iron
#[serde(rename = "block_set_type")]
pub variant: Option<String>,
}
/// One possible state of a Block.
/// This could e.g. be an extended piston facing left.
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
struct RegistryBlockState {
pub id: i32,
/// Whether this is the default state of the Block
#[serde(default, rename = "default")]
pub is_default: bool,
/// The propertise active for this `BlockState`.
#[serde(default)]
pub properties: HashMap<String, String>,
}
/// A fully-fledged block definition.
/// Stores the category, variant, all of the possible states and all of the possible properties.
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
struct RegistryBlockType {
pub definition: RegistryBlockDefinition,
pub states: Vec<RegistryBlockState>,
// TODO is this safe to remove? It's currently not used in the Project. @lukas0008 @Snowiiii
/// A list of valid property keys/values for a block.
#[serde(default, rename = "properties")]
valid_properties: HashMap<String, Vec<String>>,
}
static BLOCKS: LazyLock<HashMap<String, RegistryBlockType>> = LazyLock::new(|| {
serde_json::from_str(include_str!("../../assets/blocks.json"))
.expect("Could not parse block.json registry.")
});
fn pascal_case(original: &str) -> String {
let mut pascal = String::new();
let mut capitalize = true;
for ch in original.chars() {
if ch == '_' {
capitalize = true;
} else if capitalize {
pascal.push(ch.to_ascii_uppercase());
capitalize = false;
} else {
pascal.push(ch);
}
}
pascal
}
pub fn block_type_enum_impl() -> TokenStream {
let categories: &HashSet<&str> = &BLOCKS
.values()
.map(|val| val.definition.category.as_str())
.collect();
let original_and_converted_stream = categories.iter().map(|key| {
(
key,
pascal_case(key.split_once(':').expect("Bad minecraft id").1),
)
});
let new_names: proc_macro2::TokenStream = original_and_converted_stream
.clone()
.map(|(_, x)| x)
.join(",\n")
.parse()
.unwrap();
let from_string: proc_macro2::TokenStream = original_and_converted_stream
.clone()
.map(|(original, converted)| format!("\"{}\" => BlockCategory::{},", original, converted))
.join("\n")
.parse()
.unwrap();
// I;ve never used macros before so call me out on this lol
quote! {
#[derive(PartialEq, Clone)]
pub enum BlockCategory {
#new_names
}
impl BlockCategory {
pub fn from_registry_id(id: &str) -> BlockCategory {
match id {
#from_string
_ => panic!("Not a valid block type id"),
}
}
}
}
.into()
}
pub fn block_enum_impl() -> TokenStream {
let original_and_converted_stream = &BLOCKS.keys().map(|key| {
(
key,
pascal_case(key.split_once(':').expect("Bad minecraft id").1),
)
});
let new_names: proc_macro2::TokenStream = original_and_converted_stream
.clone()
.map(|(_, x)| x)
.join(",\n")
.parse()
.unwrap();
let from_string: proc_macro2::TokenStream = original_and_converted_stream
.clone()
.map(|(original, converted)| format!("\"{}\" => Block::{},", original, converted))
.join("\n")
.parse()
.unwrap();
// I;ve never used macros before so call me out on this lol
quote! {
#[derive(PartialEq, Clone)]
pub enum Block {
#new_names
}
impl Block {
pub fn from_registry_id(id: &str) -> Block {
match id {
#from_string
_ => panic!("Not a valid block id"),
}
}
}
}
.into()
}
pub fn block_state_impl(item: TokenStream) -> TokenStream {
let data = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated
.parse(item)
.unwrap();
let block_name = data
.first()
.expect("The first argument should be a block name");
let block_name = match block_name {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(name) => name.value(),
_ => panic!("The first argument should be a string, have: {:?}", lit),
},
_ => panic!(
"The first argument should be a string, have: {:?}",
block_name
),
};
let mut properties = HashMap::new();
for expr_thingy in data.into_iter().skip(1) {
match expr_thingy {
syn::Expr::Assign(assign) => {
let left = match assign.left.as_ref() {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(name) => name.value(),
_ => panic!(
"All not-first arguments should be assignments (\"foo\" = \"bar\")"
),
},
_ => {
panic!("All not-first arguments should be assignments (\"foo\" = \"bar\")")
}
};
let right = match assign.right.as_ref() {
syn::Expr::Lit(lit) => match &lit.lit {
syn::Lit::Str(name) => name.value(),
_ => panic!(
"All not-first arguments should be assignments (\"foo\" = \"bar\")"
),
},
_ => {
panic!("All not-first arguments should be assignments (\"foo\" = \"bar\")")
}
};
properties.insert(left, right);
}
_ => panic!("All not-first arguments should be assignments (\"foo\" = \"bar\")"),
}
}
// panic!("{:?}", properties);
let block_info = &BLOCKS
.get(&block_name)
.expect("Block with that name does not exist");
let state = if properties.is_empty() {
block_info
.states
.iter()
.find(|state| state.is_default)
.expect(
"Error inside blocks.json file: Every Block should have at least 1 default state",
)
} else {
match block_info
.states
.iter()
.find(|state| state.properties == properties)
{
Some(state) => state,
None => panic!(
"Could not find block with these properties, the following are valid properties: \n{}",
block_info
.valid_properties
.iter()
.map(|(name, values)| format!("{name} = {}", values.join(" | ")))
.join("\n")
),
}
};
let id = state.id;
let category_name = block_info.definition.category.clone();
if std::env::var("CARGO_PKG_NAME").unwrap() == "pumpkin-world" {
quote! {
crate::block::block_state::BlockState::new_unchecked(
#id as u16,
crate::block::Block::from_registry_id(#block_name),
crate::block::BlockCategory::from_registry_id(#category_name),
)
}
} else {
quote! {
pumpkin_world::block::block_id::BlockStateId::new_unchecked(
#id as u16,
pumpkin_world::block::Block::from_registry_id(#block_name),
pumpkin_world::block::BlockCategory::from_registry_id(#category_name),
)
}
}
.into()
}

View File

@@ -22,3 +22,21 @@ pub fn packet(input: TokenStream, item: TokenStream) -> TokenStream {
gen.into()
}
mod block_state;
#[proc_macro]
pub fn block(item: TokenStream) -> TokenStream {
block_state::block_state_impl(item)
}
#[proc_macro]
/// Creates an enum for all block types. Should only be used once
pub fn blocks_enum(_item: TokenStream) -> TokenStream {
block_state::block_enum_impl()
}
#[proc_macro]
/// Creates an enum for all block categories. Should only be used once
pub fn block_categories_enum(_item: TokenStream) -> TokenStream {
block_state::block_type_enum_impl()
}

View File

@@ -4,8 +4,9 @@ version.workspace = true
edition.workspace = true
[dependencies]
pumpkin-config = { path = "../pumpkin-config" }
pumpkin-macros = { path = "../pumpkin-macros" }
pumpkin-world = { path = "../pumpkin-world" }
pumpkin-world = { path = "../pumpkin-world" }
pumpkin-core = { path = "../pumpkin-core" }
bytes = "1.7"
@@ -14,7 +15,7 @@ uuid.workspace = true
serde.workspace = true
flate2 = "1.0.33"
flate2 = "1.0"
thiserror = "1.0"
log.workspace = true
@@ -25,5 +26,5 @@ num-derive = "0.4"
aes = "0.8.4"
cfb8 = "0.8.1"
itertools = "0.13.0"
itertools.workspace = true
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }

View File

@@ -1,6 +1,10 @@
### Pumpkin Protocol
Contains all Serverbound(Client->Server) and Clientbound(Server->Client) Packets.
### Features
- [x] ZLib Compression
- [x] AES/CFB8 Encryiption
Packets in the Pumpkin protocol are organized by functionality and state.
`server`: Contains definitions for serverbound packets.
@@ -69,4 +73,8 @@ Thats a Serverbound packet
pub struct CPlayDisconnect {
reason: TextComponent,
}
``
```
### Porting
You can compare difference in Protocol on wiki.vg https://wiki.vg/index.php?title=Protocol&action=history
Also change the `CURRENT_MC_PROTOCOL` in `src/lib.rs`

View File

@@ -72,19 +72,19 @@ impl ByteBuffer {
}
pub fn get_string(&mut self) -> Result<String, DeserializerError> {
self.get_string_len(32767)
self.get_string_len(i16::MAX as i32)
}
pub fn get_string_len(&mut self, max_size: usize) -> Result<String, DeserializerError> {
pub fn get_string_len(&mut self, max_size: i32) -> Result<String, DeserializerError> {
let size = self.get_var_int()?.0;
if size as usize > max_size {
if size > max_size {
return Err(DeserializerError::Message(
"String length is bigger than max size".to_string(),
));
}
let data = self.copy_to_bytes(size as usize)?;
if data.len() > max_size {
if data.len() as i32 > max_size {
return Err(DeserializerError::Message(
"String is bigger than max size".to_string(),
));
@@ -125,6 +125,14 @@ impl ByteBuffer {
}
pub fn put_string(&mut self, val: &str) {
self.put_string_len(val, i16::MAX as i32);
}
pub fn put_string_len(&mut self, val: &str, max_size: i32) {
if val.len() as i32 > max_size {
// Should be panic?, I mean its our fault
panic!("String is too big");
}
self.put_var_int(&val.len().into());
self.buffer.put(val.as_bytes());
}

View File

@@ -2,12 +2,10 @@ use pumpkin_core::text::TextComponent;
use pumpkin_macros::packet;
use serde::Serialize;
use crate::uuid::UUID;
#[derive(Serialize)]
#[packet(0x09)]
pub struct CConfigAddResourcePack<'a> {
uuid: UUID,
uuid: uuid::Uuid,
url: &'a str,
hash: &'a str, // max 40
forced: bool,
@@ -16,7 +14,7 @@ pub struct CConfigAddResourcePack<'a> {
impl<'a> CConfigAddResourcePack<'a> {
pub fn new(
uuid: UUID,
uuid: uuid::Uuid,
url: &'a str,
hash: &'a str,
forced: bool,

View File

@@ -1,4 +1,3 @@
use num_derive::ToPrimitive;
use pumpkin_macros::packet;
use serde::Serialize;
@@ -21,7 +20,7 @@ impl CEntityAnimation {
}
}
#[derive(ToPrimitive)]
#[repr(u8)]
pub enum Animation {
SwingMainArm,
LeaveBed,

View File

@@ -0,0 +1,18 @@
use pumpkin_macros::packet;
use serde::Serialize;
#[derive(Serialize)]
#[packet(0x1F)]
pub struct CEntityStatus {
entity_id: i32,
entity_status: i8,
}
impl CEntityStatus {
pub fn new(entity_id: i32, entity_status: i8) -> Self {
Self {
entity_id,
entity_status,
}
}
}

View File

@@ -8,8 +8,30 @@ pub struct CGameEvent {
value: f32,
}
/// Somewhere you need to implement all the random stuff right?
impl CGameEvent {
pub fn new(event: u8, value: f32) -> Self {
Self { event, value }
pub fn new(event: GameEvent, value: f32) -> Self {
Self {
event: event as u8,
value,
}
}
}
#[repr(u8)]
pub enum GameEvent {
NoRespawnBlockAvailable,
BeginRaining,
EndRaining,
ChangeGameMode,
WinGame,
DemoEvent,
ArrowHitPlayer,
RainLevelChange,
ThunderLevelChange,
PlayPufferfishStringSound,
PlayElderGuardianMobAppearance,
EnabledRespawnScreen,
LimitedCrafting,
StartWaitingChunks,
}

View File

@@ -0,0 +1,8 @@
use pumpkin_macros::packet;
use serde::Serialize;
#[packet(0x26)]
#[derive(Serialize)]
pub struct CKeepAlive {
pub keep_alive_id: i64,
}

View File

@@ -2,11 +2,12 @@ use pumpkin_core::text::TextComponent;
use pumpkin_macros::packet;
use serde::Serialize;
use crate::{uuid::UUID, BitSet, VarInt};
use crate::{BitSet, VarInt};
#[derive(Serialize)]
#[packet(0x39)]
pub struct CPlayerChatMessage<'a> {
sender: UUID,
#[serde(with = "uuid::serde::compact")]
sender: uuid::Uuid,
index: VarInt,
message_signature: Option<&'a [u8]>,
message: &'a str,
@@ -24,7 +25,7 @@ pub struct CPlayerChatMessage<'a> {
impl<'a> CPlayerChatMessage<'a> {
#[expect(clippy::too_many_arguments)]
pub fn new(
sender: UUID,
sender: uuid::Uuid,
index: VarInt,
message_signature: Option<&'a [u8]>,
message: &'a str,

View File

@@ -7,12 +7,12 @@ use super::PlayerAction;
#[packet(0x3E)]
pub struct CPlayerInfoUpdate<'a> {
pub actions: i8,
pub players: &'a [Player],
pub players: &'a [Player<'a>],
}
pub struct Player {
pub struct Player<'a> {
pub uuid: uuid::Uuid,
pub actions: Vec<PlayerAction>,
pub actions: Vec<PlayerAction<'a>>,
}
impl<'a> CPlayerInfoUpdate<'a> {

View File

@@ -1,20 +1,32 @@
use pumpkin_macros::packet;
use serde::Serialize;
use serde::{ser::SerializeSeq, Serialize};
use crate::{uuid::UUID, VarInt};
use crate::VarInt;
#[derive(Serialize)]
#[packet(0x3D)]
pub struct CRemovePlayerInfo<'a> {
players_count: VarInt,
players: &'a [UUID],
#[serde(serialize_with = "serialize_slice_uuids")]
players: &'a [uuid::Uuid],
}
impl<'a> CRemovePlayerInfo<'a> {
pub fn new(players_count: VarInt, players: &'a [UUID]) -> Self {
pub fn new(players_count: VarInt, players: &'a [uuid::Uuid]) -> Self {
Self {
players_count,
players,
}
}
}
fn serialize_slice_uuids<S: serde::Serializer>(
uuids: &[uuid::Uuid],
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(uuids.len()))?;
for uuid in uuids {
seq.serialize_element(uuid.as_bytes())?;
}
seq.end()
}

View File

@@ -0,0 +1,22 @@
use pumpkin_macros::packet;
use serde::Serialize;
use crate::VarInt;
#[derive(Serialize)]
#[packet(0x5D)]
pub struct CSetHealth {
health: f32,
food: VarInt,
food_saturation: f32,
}
impl CSetHealth {
pub fn new(health: f32, food: VarInt, food_saturation: f32) -> Self {
Self {
health,
food,
food_saturation,
}
}
}

View File

@@ -1,13 +1,14 @@
use pumpkin_macros::packet;
use serde::Serialize;
use crate::{uuid::UUID, VarInt};
use crate::VarInt;
#[derive(Serialize)]
#[packet(0x01)]
pub struct CSpawnEntity {
entity_id: VarInt,
entity_uuid: UUID,
#[serde(with = "uuid::serde::compact")]
entity_uuid: uuid::Uuid,
typ: VarInt,
x: f64,
y: f64,
@@ -25,7 +26,7 @@ impl CSpawnEntity {
#[expect(clippy::too_many_arguments)]
pub fn new(
entity_id: VarInt,
entity_uuid: UUID,
entity_uuid: uuid::Uuid,
typ: VarInt,
x: f64,
y: f64,

View File

@@ -9,10 +9,12 @@ mod c_close_container;
mod c_disguised_chat_message;
mod c_entity_animation;
mod c_entity_metadata;
mod c_entity_status;
mod c_entity_velocity;
mod c_game_event;
mod c_head_rot;
mod c_hurt_animation;
mod c_keep_alive;
mod c_login;
mod c_open_screen;
mod c_particle;
@@ -26,6 +28,7 @@ mod c_remove_entities;
mod c_set_container_content;
mod c_set_container_property;
mod c_set_container_slot;
mod c_set_health;
mod c_set_held_item;
mod c_set_title;
mod c_spawn_player;
@@ -51,10 +54,12 @@ pub use c_close_container::*;
pub use c_disguised_chat_message::*;
pub use c_entity_animation::*;
pub use c_entity_metadata::*;
pub use c_entity_status::*;
pub use c_entity_velocity::*;
pub use c_game_event::*;
pub use c_head_rot::*;
pub use c_hurt_animation::*;
pub use c_keep_alive::*;
pub use c_login::*;
pub use c_open_screen::*;
pub use c_particle::*;
@@ -68,6 +73,7 @@ pub use c_remove_entities::*;
pub use c_set_container_content::*;
pub use c_set_container_property::*;
pub use c_set_container_slot::*;
pub use c_set_health::*;
pub use c_set_held_item::*;
pub use c_set_title::*;
pub use c_spawn_player::*;

View File

@@ -1,9 +1,9 @@
use crate::{Property, VarInt};
pub enum PlayerAction {
pub enum PlayerAction<'a> {
AddPlayer {
name: String,
properties: Vec<Property>,
name: &'a str,
properties: &'a [Property],
},
InitializeChat(u8),
/// Gamemode ?

View File

@@ -10,8 +10,9 @@ pub mod packet_decoder;
pub mod packet_encoder;
pub mod server;
pub mod slot;
pub mod uuid;
/// To current Minecraft protocol
/// Don't forget to change this when porting
pub const CURRENT_MC_PROTOCOL: u32 = 767;
pub const MAX_PACKET_SIZE: i32 = 2097152;
@@ -151,7 +152,7 @@ pub enum PacketError {
MalformedLength,
}
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ConnectionState {
HandShake,
Status,
@@ -175,7 +176,6 @@ impl From<VarInt> for ConnectionState {
}
}
}
pub struct RawPacket {
pub id: VarInt,
pub bytebuf: ByteBuffer,
@@ -191,32 +191,45 @@ pub trait ServerPacket: Packet + Sized {
#[derive(Serialize)]
pub struct StatusResponse {
pub version: Version,
pub players: Players,
/// The version on which the Server is running. Optional
pub version: Option<Version>,
/// Information about currently connected Players. Optional
pub players: Option<Players>,
/// The description displayed also called MOTD (Message of the day). Optional
pub description: String,
pub favicon: Option<String>, // data:image/png;base64,<data>
// Players, favicon ...
/// The icon displayed, Optional
pub favicon: Option<String>,
/// Players are forced to use Secure chat
pub enforce_secure_chat: bool,
}
#[derive(Serialize)]
pub struct Version {
/// The current name of the Version (e.g. 1.21.1)
pub name: String,
/// The current Protocol Version (e.g. 767)
pub protocol: u32,
}
#[derive(Serialize)]
pub struct Players {
/// The maximum Player count the server allows
pub max: u32,
/// The current online player count
pub online: u32,
/// Information about currently connected players.
/// Note player can disable listing here.
pub sample: Vec<Sample>,
}
#[derive(Serialize)]
pub struct Sample {
/// Players Name
pub name: String,
pub id: String, // uuid
/// Players UUID
pub id: String,
}
// basicly game profile
// basically game profile
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Property {
pub name: String,

View File

@@ -13,11 +13,13 @@ use crate::{
type Cipher = cfb8::Decryptor<aes::Aes128>;
// Decoder: Client -> Server
// Supports ZLib decoding/decompression
// Supports Aes128 Encyption
#[derive(Default)]
pub struct PacketDecoder {
buf: BytesMut,
decompress_buf: BytesMut,
compression: Option<u32>,
compression: bool,
cipher: Option<Cipher>,
}
@@ -43,7 +45,7 @@ impl PacketDecoder {
let packet_len_len = VarInt(packet_len).written_size();
let mut data;
if self.compression.is_some() {
if self.compression {
r = &r[..packet_len as usize];
let data_len = VarInt::decode(&mut r).map_err(|_| PacketError::TooLong)?.0;
@@ -94,18 +96,26 @@ impl PacketDecoder {
}))
}
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
assert!(self.cipher.is_none(), "encryption is already enabled");
pub fn set_encryption(&mut self, key: Option<&[u8; 16]>) {
if let Some(key) = key {
assert!(self.cipher.is_none(), "encryption is already enabled");
let mut cipher = Cipher::new_from_slices(key, key).expect("invalid key");
let mut cipher = Cipher::new_from_slices(key, key).expect("invalid key");
// Don't forget to decrypt the data we already have.
Self::decrypt_bytes(&mut cipher, &mut self.buf);
// Don't forget to decrypt the data we already have.
self.cipher = Some(cipher);
Self::decrypt_bytes(&mut cipher, &mut self.buf);
self.cipher = Some(cipher);
} else {
assert!(self.cipher.is_some(), "encryption is already disabled");
self.cipher = None;
}
}
pub fn set_compression(&mut self, compression: Option<u32>) {
/// Sets ZLib Deompression
pub fn set_compression(&mut self, compression: bool) {
self.compression = compression;
}

View File

@@ -2,6 +2,7 @@ use std::io::Write;
use aes::cipher::{generic_array::GenericArray, BlockEncryptMut, BlockSizeUser, KeyIvInit};
use bytes::{BufMut, BytesMut};
use pumpkin_config::compression::CompressionInfo;
use std::io::Read;
@@ -13,18 +14,19 @@ use crate::{bytebuf::ByteBuffer, ClientPacket, PacketError, VarInt, MAX_PACKET_S
type Cipher = cfb8::Encryptor<aes::Aes128>;
// Encoder: Server -> Client
// Supports ZLib endecoding/compression
// Supports Aes128 Encyption
#[derive(Default)]
pub struct PacketEncoder {
buf: BytesMut,
compress_buf: Vec<u8>,
compression: Option<(u32, u32)>,
compression: Option<CompressionInfo>,
cipher: Option<Cipher>,
}
impl PacketEncoder {
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();
let mut packet_buf = ByteBuffer::empty();
@@ -39,10 +41,10 @@ impl PacketEncoder {
let data_len = self.buf.len() - start_len;
if let Some((threshold, compression_level)) = self.compression {
if data_len > threshold as usize {
if let Some(compression) = &self.compression {
if data_len > compression.threshold as usize {
let mut z =
ZlibEncoder::new(&self.buf[start_len..], Compression::new(compression_level));
ZlibEncoder::new(&self.buf[start_len..], Compression::new(compression.level));
self.compress_buf.clear();
@@ -85,7 +87,6 @@ impl PacketEncoder {
let mut front = &mut self.buf[start_len..];
#[allow(clippy::needless_borrows_for_generic_args)]
VarInt(packet_len as i32)
.encode(&mut front)
.map_err(|_| PacketError::EncodeLength)?;
@@ -117,12 +118,20 @@ impl PacketEncoder {
Ok(())
}
pub fn enable_encryption(&mut self, key: &[u8; 16]) {
assert!(self.cipher.is_none(), "encryption is already enabled");
self.cipher = Some(Cipher::new_from_slices(key, key).expect("invalid key"));
pub fn set_encryption(&mut self, key: Option<&[u8; 16]>) {
if let Some(key) = key {
assert!(self.cipher.is_none(), "encryption is already enabled");
self.cipher = Some(Cipher::new_from_slices(key, key).expect("invalid key"));
} else {
assert!(self.cipher.is_some(), "encryption is disabled");
self.cipher = None;
}
}
pub fn set_compression(&mut self, compression: Option<(u32, u32)>) {
/// Enables ZLib Compression
pub fn set_compression(&mut self, compression: Option<CompressionInfo>) {
self.compression = compression;
}

View File

@@ -14,7 +14,7 @@ pub struct SPluginMessage {
impl ServerPacket for SPluginMessage {
fn read(bytebuf: &mut ByteBuffer) -> Result<Self, DeserializerError> {
Ok(Self {
channel: bytebuf.get_string().unwrap(),
channel: bytebuf.get_string()?,
data: bytebuf.get_slice().to_vec(),
})
}

View File

@@ -9,7 +9,6 @@ use crate::{
#[packet(0x02)]
pub struct SLoginPluginResponse {
pub message_id: VarInt,
pub successful: bool,
pub data: Option<BytesMut>,
}
@@ -17,7 +16,6 @@ impl ServerPacket for SLoginPluginResponse {
fn read(bytebuf: &mut ByteBuffer) -> Result<Self, DeserializerError> {
Ok(Self {
message_id: bytebuf.get_var_int()?,
successful: bytebuf.get_bool()?,
data: bytebuf.get_option(|v| Ok(v.get_slice()))?,
})
}

View File

@@ -5,6 +5,7 @@ mod s_client_information;
mod s_close_container;
mod s_confirm_teleport;
mod s_interact;
mod s_keep_alive;
mod s_ping_request;
mod s_player_action;
mod s_player_command;
@@ -25,6 +26,7 @@ pub use s_client_information::*;
pub use s_close_container::*;
pub use s_confirm_teleport::*;
pub use s_interact::*;
pub use s_keep_alive::*;
pub use s_ping_request::*;
pub use s_player_action::*;
pub use s_player_command::*;

View File

@@ -0,0 +1,8 @@
use pumpkin_macros::packet;
use serde::Deserialize;
#[packet(0x18)]
#[derive(Deserialize)]
pub struct SKeepAlive {
pub keep_alive_id: i64,
}

View File

@@ -1,13 +0,0 @@
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

@@ -4,8 +4,8 @@ version.workspace = true
edition.workspace = true
[dependencies]
pumpkin-protocol = { path = "../pumpkin-protocol"}
pumpkin-core = { path = "../pumpkin-core"}
pumpkin-protocol = { path = "../pumpkin-protocol" }
pumpkin-core = { path = "../pumpkin-core" }
# nbt
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }

View File

@@ -4,13 +4,14 @@ version.workspace = true
edition.workspace = true
[dependencies]
pumpkin-core = { path = "../pumpkin-core"}
pumpkin-core = { path = "../pumpkin-core" }
pumpkin-macros = { path = "../pumpkin-macros" }
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }
tokio.workspace = true
rayon.workspace = true
derive_more.workspace = true
itertools = "0.13.0"
itertools.workspace = true
thiserror = "1.0"
futures = "0.3"
flate2 = "1.0"
@@ -19,6 +20,8 @@ serde_json = "1.0"
static_assertions = "1.1.0"
log.workspace = true
parking_lot.workspace = true
noise = "0.9.0"
rand = "0.8.5"

17
pumpkin-world/README.md Normal file
View File

@@ -0,0 +1,17 @@
### Pumpkin World
Contains everything World related for example
- Loading Chunks (Anvil Format)
- Generating Chunks
- Loading Blocks/Items
### Porting
When updating your Minecraft server to a newer version, you typically need to replace the files in the assets directory to ensure compatibility with the new version's resources.
Thankfully, vanilla Minecraft provides a way to extract these updated assets directly from the server JAR file itself.
1. Download the latest Minecraft server JAR file for the version you want to upgrade to.
2. Run `java -DbundlerMainClass=net.minecraft.data.Main -jar <minecraft_server>.jar --reports`.
3. This command will create a new folder named `reports` in the same directory as the server JAR. This folder contains the updated "assets" directory for the new version.
4. Copy the assets folder from the reports folder and replace the existing assets directory within your server directory.
For details see https://wiki.vg/Data_Generators

View File

@@ -1,58 +0,0 @@
use std::collections::HashMap;
use serde::Deserialize;
use super::block_registry::BLOCKS;
use crate::level::WorldError;
// 0 is air -> reasonable default
#[derive(Default, Deserialize, Debug, Hash, Clone, Copy, PartialEq, Eq)]
#[serde(transparent)]
pub struct BlockId {
data: u16,
}
impl BlockId {
pub const AIR: Self = Self::from_id(0);
pub fn new(
text_id: &str,
properties: Option<&HashMap<String, String>>,
) -> Result<Self, WorldError> {
let mut block_states = BLOCKS
.get(text_id)
.ok_or(WorldError::BlockIdentifierNotFound)?
.states
.iter();
let block_state = match properties {
Some(properties) => match block_states.find(|state| &state.properties == properties) {
Some(state) => state,
None => return Err(WorldError::BlockStateIdNotFound),
},
None => block_states
.find(|state| state.is_default)
.expect("Every Block should have at least 1 default state"),
};
Ok(block_state.id)
}
pub const fn from_id(id: u16) -> Self {
// TODO: add check if the id is actually valid
Self { data: id }
}
pub fn is_air(&self) -> bool {
self.data == 0 || self.data == 12959 || self.data == 12958
}
pub fn get_id(&self) -> u16 {
self.data
}
/// An i32 is the way mojang internally represents their Blocks
pub fn get_id_mojang_repr(&self) -> i32 {
self.data as i32
}
}

View File

@@ -2,13 +2,16 @@ use std::{collections::HashMap, sync::LazyLock};
use serde::Deserialize;
use super::block_id::BlockId;
use super::BlockState;
pub static BLOCKS: LazyLock<HashMap<String, RegistryBlockType>> = LazyLock::new(|| {
serde_json::from_str(include_str!("../../assets/blocks.json"))
serde_json::from_str(include_str!("../../../assets/blocks.json"))
.expect("Could not parse block.json registry.")
});
pumpkin_macros::blocks_enum!();
pumpkin_macros::block_categories_enum!();
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct RegistryBlockDefinition {
/// e.g. minecraft:door or minecraft:button
@@ -48,3 +51,31 @@ pub struct RegistryBlockType {
#[serde(default, rename = "properties")]
valid_properties: HashMap<String, Vec<String>>,
}
#[derive(Default, Copy, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct BlockId {
pub data: u16,
}
impl BlockId {
pub fn is_air(&self) -> bool {
self.data == 0 || self.data == 12959 || self.data == 12958
}
pub fn get_id_mojang_repr(&self) -> i32 {
self.data as i32
}
pub fn get_id(&self) -> u16 {
self.data
}
}
impl From<BlockState> for BlockId {
fn from(value: BlockState) -> Self {
Self {
data: value.get_id(),
}
}
}

View File

@@ -0,0 +1,73 @@
use std::collections::HashMap;
use crate::level::WorldError;
use super::block_registry::{Block, BlockCategory, BLOCKS};
#[derive(Clone)]
pub struct BlockState {
state_id: u16,
block: Block,
category: BlockCategory,
}
impl BlockState {
pub const AIR: BlockState = BlockState {
state_id: 0,
block: Block::Air,
category: BlockCategory::Air,
};
pub fn new(
registry_id: &str,
properties: Option<&HashMap<String, String>>,
) -> Result<Self, WorldError> {
let block_registry = BLOCKS
.get(registry_id)
.ok_or(WorldError::BlockIdentifierNotFound)?;
let mut block_states = block_registry.states.iter();
let block_state = match properties {
Some(properties) => block_states
.find(|state| &state.properties == properties)
.ok_or_else(|| WorldError::BlockStateIdNotFound)?,
None => block_states
.find(|state| state.is_default)
.expect("Every Block should have at least 1 default state"),
};
Ok(Self {
state_id: block_state.id.data,
block: Block::from_registry_id(registry_id),
category: BlockCategory::from_registry_id(&block_registry.definition.category),
})
}
pub const fn new_unchecked(state_id: u16, block: Block, category: BlockCategory) -> Self {
Self {
state_id,
block,
category,
}
}
pub fn is_air(&self) -> bool {
self.category == BlockCategory::Air
}
pub fn get_id(&self) -> u16 {
self.state_id
}
pub fn get_id_mojang_repr(&self) -> i32 {
self.state_id as i32
}
pub fn of_block(&self, block: Block) -> bool {
self.block == block
}
pub fn of_category(&self, category: BlockCategory) -> bool {
self.category == category
}
}

View File

@@ -1,11 +1,13 @@
use num_derive::FromPrimitive;
pub mod block_id;
mod block_registry;
pub mod block_state;
pub use block_id::BlockId;
use pumpkin_core::math::vector3::Vector3;
pub use block_registry::{Block, BlockCategory, BlockId};
pub use block_state::BlockState;
#[derive(FromPrimitive)]
pub enum BlockFace {
Bottom = 0,

View File

@@ -7,7 +7,7 @@ use pumpkin_core::math::vector2::Vector2;
use serde::{Deserialize, Serialize};
use crate::{
block::BlockId,
block::{BlockId, BlockState},
coordinates::{ChunkRelativeBlockCoordinates, Height},
level::{ChunkNotGeneratedError, WorldError},
WORLD_HEIGHT,
@@ -215,7 +215,12 @@ impl ChunkData {
let palette = block_states
.palette
.iter()
.map(|entry| BlockId::new(&entry.name, entry.properties.as_ref()))
.map(
|entry| match BlockState::new(&entry.name, entry.properties.as_ref()) {
Err(e) => Err(e),
Ok(state) => Ok(state.into()),
},
)
.collect::<Result<Vec<_>, _>>()?;
let block_data = match block_states.data {

View File

@@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::LazyLock};
pub const ITEM_REGISTRY: &str = "minecraft:item";
const REGISTRY_JSON: &str = include_str!("../assets/registries.json");
const REGISTRY_JSON: &str = include_str!("../../assets/registries.json");
#[derive(serde::Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct RegistryElement {

View File

@@ -3,7 +3,7 @@ use std::{collections::HashMap, sync::LazyLock};
use super::Rarity;
use crate::global_registry::{self, ITEM_REGISTRY};
const ITEMS_JSON: &str = include_str!("../../assets/items.json");
const ITEMS_JSON: &str = include_str!("../../../assets/items.json");
pub static ITEMS: LazyLock<HashMap<String, ItemElement>> = LazyLock::new(|| {
serde_json::from_str(ITEMS_JSON).expect("Could not parse items.json registry.")

View File

@@ -3,11 +3,12 @@ use std::{
fs::OpenOptions,
io::{Read, Seek},
path::PathBuf,
sync::{Arc, Mutex},
sync::Arc,
};
use flate2::{bufread::ZlibDecoder, read::GzDecoder};
use itertools::Itertools;
use parking_lot::Mutex;
use pumpkin_core::math::vector2::Vector2;
use rayon::prelude::*;
use thiserror::Error;
@@ -18,7 +19,15 @@ use crate::{
world_gen::{get_world_gen, Seed, WorldGenerator},
};
/// The Level represents a single Dimension.
/// The `Level` module provides functionality for working with chunks within or outside a Minecraft world.
///
/// Key features include:
///
/// - **Chunk Loading:** Efficiently loads chunks from disk (Anvil format).
/// - **Chunk Caching:** Stores accessed chunks in memory for faster access.
/// - **Chunk Generation:** Generates new chunks on-demand using a specified `WorldGenerator`.
///
/// For more details on world generation, refer to the `WorldGenerator` module.
pub struct Level {
save_file: Option<SaveFile>,
loaded_chunks: Arc<Mutex<HashMap<Vector2<i32>, Arc<ChunkData>>>>,
@@ -126,16 +135,7 @@ impl Level {
}
}
// /// Read one chunk in the world
// ///
// /// Do not use this function if reading many chunks is required, since in case those two chunks which are read separately using `.read_chunk` are in the same region file, it will need to be opened and closed separately for both of them, leading to a performance loss.
// pub async fn read_chunk(&self, chunk: (i32, i32)) -> Result<ChunkData, WorldError> {
// self.read_chunks(vec![chunk])
// .await
// .pop()
// .expect("Read chunks must return a chunk")
// .1
// }
pub fn get_block() {}
/// Reads/Generates many chunks in a world
/// MUST be called from a tokio runtime thread
@@ -149,43 +149,41 @@ impl Level {
) {
chunks.into_par_iter().for_each(|at| {
if is_alive {
dbg!("a");
return;
}
if let Ok(mut loaded_chunks) = self.loaded_chunks.lock() {
let channel = channel.clone();
let mut loaded_chunks = self.loaded_chunks.lock();
let channel = channel.clone();
// Check if chunks is already loaded
if loaded_chunks.contains_key(at) {
channel
.blocking_send(Ok(loaded_chunks.get(at).unwrap().clone()))
.expect("Failed sending ChunkData.");
return;
}
let at = *at;
let data = match &self.save_file {
Some(save_file) => {
match Self::read_chunk(save_file, at) {
Err(WorldError::ChunkNotGenerated(_)) => {
// This chunk was not generated yet.
Ok(self.world_gen.generate_chunk(at))
}
// TODO this doesn't warn the user about the error. fix.
result => result,
}
}
None => {
// There is no savefile yet -> generate the chunks
Ok(self.world_gen.generate_chunk(at))
}
}
.unwrap();
let data = Arc::new(data);
// Check if chunks is already loaded
if loaded_chunks.contains_key(at) {
channel
.blocking_send(Ok(data.clone()))
.blocking_send(Ok(loaded_chunks.get(at).unwrap().clone()))
.expect("Failed sending ChunkData.");
loaded_chunks.insert(at, data);
return;
}
let at = *at;
let data = match &self.save_file {
Some(save_file) => {
match Self::read_chunk(save_file, at) {
Err(WorldError::ChunkNotGenerated(_)) => {
// This chunk was not generated yet.
Ok(self.world_gen.generate_chunk(at))
}
// TODO this doesn't warn the user about the error. fix.
result => result,
}
}
None => {
// There is no savefile yet -> generate the chunks
Ok(self.world_gen.generate_chunk(at))
}
}
.unwrap();
let data = Arc::new(data);
channel
.blocking_send(Ok(data.clone()))
.expect("Failed sending ChunkData.");
loaded_chunks.insert(at, data);
})
}
@@ -238,29 +236,21 @@ impl Level {
// Read the file using the offset and size
let mut file_buf = {
let seek_result = region_file.seek(std::io::SeekFrom::Start(offset));
if seek_result.is_err() {
return Err(WorldError::RegionIsInvalid);
}
region_file
.seek(std::io::SeekFrom::Start(offset))
.map_err(|_| WorldError::RegionIsInvalid)?;
let mut out = vec![0; size];
let read_result = region_file.read_exact(&mut out);
if read_result.is_err() {
return Err(WorldError::RegionIsInvalid);
}
region_file
.read_exact(&mut out)
.map_err(|_| WorldError::RegionIsInvalid)?;
out
};
// TODO: check checksum to make sure chunk is not corrupted
let header = file_buf.drain(0..5).collect_vec();
let compression = match Compression::from_byte(header[4]) {
Some(c) => c,
None => {
return Err(WorldError::Compression(
CompressionError::UnknownCompression,
))
}
};
let compression = Compression::from_byte(header[4])
.ok_or_else(|| WorldError::Compression(CompressionError::UnknownCompression))?;
let size = u32::from_be_bytes(header[..4].try_into().unwrap());
@@ -280,23 +270,15 @@ impl Level {
Compression::Gzip => {
let mut z = GzDecoder::new(&compressed_data[..]);
let mut chunk_data = Vec::with_capacity(compressed_data.len());
match z.read_to_end(&mut chunk_data) {
Ok(_) => {}
Err(e) => {
return Err(CompressionError::GZipError(e));
}
}
z.read_to_end(&mut chunk_data)
.map_err(CompressionError::GZipError)?;
Ok(chunk_data)
}
Compression::Zlib => {
let mut z = ZlibDecoder::new(&compressed_data[..]);
let mut chunk_data = Vec::with_capacity(compressed_data.len());
match z.read_to_end(&mut chunk_data) {
Ok(_) => {}
Err(e) => {
return Err(CompressionError::ZlibError(e));
}
}
z.read_to_end(&mut chunk_data)
.map_err(CompressionError::ZlibError)?;
Ok(chunk_data)
}
Compression::None => Ok(compressed_data),

View File

@@ -3,7 +3,7 @@ use pumpkin_core::math::vector2::Vector2;
use static_assertions::assert_obj_safe;
use crate::biome::Biome;
use crate::block::BlockId;
use crate::block::block_state::BlockState;
use crate::chunk::ChunkData;
use crate::coordinates::{BlockCoordinates, XZBlockCoordinates};
use crate::world_gen::Seed;
@@ -26,12 +26,12 @@ pub(crate) trait TerrainGenerator: Sync + Send {
fn prepare_chunk(&self, at: &Vector2<i32>);
/// Is static
fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockId;
fn generate_block(&self, at: BlockCoordinates, biome: Biome) -> BlockState;
}
pub(crate) trait PerlinTerrainGenerator: Sync + Send {
fn prepare_chunk(&self, at: &Vector2<i32>, perlin: &Perlin);
/// Dependens on the perlin noise height
fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, biome: Biome) -> BlockId;
fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, biome: Biome) -> BlockState;
}

View File

@@ -62,11 +62,13 @@ impl<B: BiomeGenerator, T: PerlinTerrainGenerator> WorldGenerator for GenericGen
blocks.set_block(
coordinates,
self.terrain_generator.generate_block(
coordinates.with_chunk_coordinates(at),
chunk_height as i16,
biome,
),
self.terrain_generator
.generate_block(
coordinates.with_chunk_coordinates(at),
chunk_height as i16,
biome,
)
.into(),
);
}
}

View File

@@ -3,7 +3,7 @@ use pumpkin_core::math::vector2::Vector2;
use crate::{
biome::Biome,
block::BlockId,
block::block_state::BlockState,
coordinates::{BlockCoordinates, XZBlockCoordinates},
world_gen::{
generator::{BiomeGenerator, GeneratorInit, PerlinTerrainGenerator},
@@ -40,21 +40,21 @@ impl GeneratorInit for PlainsTerrainGenerator {
impl PerlinTerrainGenerator for PlainsTerrainGenerator {
fn prepare_chunk(&self, _at: &Vector2<i32>, _perlin: &Perlin) {}
// TODO allow specifying which blocks should be at which height in the config.
fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, _: Biome) -> BlockId {
fn generate_block(&self, at: BlockCoordinates, chunk_height: i16, _: Biome) -> BlockState {
let begin_stone_height = chunk_height - 5;
let begin_dirt_height = chunk_height - 1;
let y = *at.y;
if y == -64 {
BlockId::from_id(79) // BEDROCK
pumpkin_macros::block!("minecraft:bedrock")
} else if y >= -63 && y <= begin_stone_height {
return BlockId::from_id(1); // STONE
pumpkin_macros::block!("minecraft:stone")
} else if y >= begin_stone_height && y < begin_dirt_height {
return BlockId::from_id(10); // DIRT;
pumpkin_macros::block!("minecraft:dirt")
} else if y == chunk_height - 1 {
return BlockId::from_id(9); // GRASS BLOCK
pumpkin_macros::block!("minecraft:grass_block")
} else {
BlockId::AIR
BlockState::AIR
}
}
}

View File

@@ -1,8 +1,9 @@
use pumpkin_core::math::vector2::Vector2;
use pumpkin_macros::block;
use crate::{
biome::Biome,
block::BlockId,
block::block_state::BlockState,
coordinates::{BlockCoordinates, XZBlockCoordinates},
world_gen::{
generator::{BiomeGenerator, GeneratorInit, TerrainGenerator},
@@ -40,12 +41,12 @@ impl GeneratorInit for SuperflatTerrainGenerator {
impl TerrainGenerator for SuperflatTerrainGenerator {
fn prepare_chunk(&self, _at: &Vector2<i32>) {}
// TODO allow specifying which blocks should be at which height in the config.
fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockId {
fn generate_block(&self, at: BlockCoordinates, _: Biome) -> BlockState {
match *at.y {
-64 => BlockId::from_id(79), // Bedrock
-63..=-62 => BlockId::from_id(10), // Dirt
-61 => BlockId::from_id(9), // Grass
_ => BlockId::AIR,
-64 => block!("minecraft:bedrock"),
-63..=-62 => block!("minecraft:dirt"),
-61 => block!("minecraft:grass_block"),
_ => BlockState::AIR,
}
}
}

View File

@@ -1,6 +1,7 @@
mod generator;
mod generic_generator;
mod implementation;
mod noise;
mod seed;
pub use generator::WorldGenerator;

View File

@@ -0,0 +1,65 @@
#![allow(dead_code)]
mod perlin;
mod simplex;
pub fn lerp(delta: f64, start: f64, end: f64) -> f64 {
start + delta * (end - start)
}
pub fn lerp2(delta_x: f64, delta_y: f64, x0y0: f64, x1y0: f64, x0y1: f64, x1y1: f64) -> f64 {
lerp(
delta_y,
lerp(delta_x, x0y0, x1y0),
lerp(delta_x, x0y1, x1y1),
)
}
#[allow(clippy::too_many_arguments)]
pub fn lerp3(
delta_x: f64,
delta_y: f64,
delta_z: f64,
x0y0z0: f64,
x1y0z0: f64,
x0y1z0: f64,
x1y1z0: f64,
x0y0z1: f64,
x1y0z1: f64,
x0y1z1: f64,
x1y1z1: f64,
) -> f64 {
lerp(
delta_z,
lerp2(delta_x, delta_y, x0y0z0, x1y0z0, x0y1z0, x1y1z0),
lerp2(delta_x, delta_y, x0y0z1, x1y0z1, x0y1z1, x1y1z1),
)
}
struct Gradient {
x: i32,
y: i32,
z: i32,
}
const GRADIENTS: [Gradient; 16] = [
Gradient { x: 1, y: 1, z: 0 },
Gradient { x: -1, y: 1, z: 0 },
Gradient { x: 1, y: -1, z: 0 },
Gradient { x: -1, y: -1, z: 0 },
Gradient { x: 1, y: 0, z: 1 },
Gradient { x: -1, y: 0, z: 1 },
Gradient { x: 1, y: 0, z: -1 },
Gradient { x: -1, y: 0, z: -1 },
Gradient { x: 0, y: 1, z: 1 },
Gradient { x: 0, y: -1, z: 1 },
Gradient { x: 0, y: 1, z: -1 },
Gradient { x: 0, y: -1, z: -1 },
Gradient { x: 1, y: 1, z: 0 },
Gradient { x: 0, y: -1, z: 1 },
Gradient { x: -1, y: 1, z: 0 },
Gradient { x: 0, y: -1, z: -1 },
];
fn dot(gradient: &Gradient, x: f64, y: f64, z: f64) -> f64 {
gradient.x as f64 * x + gradient.y as f64 * y + gradient.z as f64 * z
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,702 @@
use num_traits::Pow;
use pumpkin_core::random::{legacy_rand::LegacyRand, RandomImpl};
use super::{dot, GRADIENTS};
pub struct SimplexNoiseSampler {
permutation: Box<[u8]>,
x_origin: f64,
y_origin: f64,
z_origin: f64,
}
impl SimplexNoiseSampler {
const SQRT_3: f64 = 1.7320508075688772f64;
const SKEW_FACTOR_2D: f64 = 0.5f64 * (Self::SQRT_3 - 1f64);
const UNSKEW_FACTOR_2D: f64 = (3f64 - Self::SQRT_3) / 6f64;
pub fn new(random: &mut impl RandomImpl) -> Self {
let x_origin = random.next_f64() * 256f64;
let y_origin = random.next_f64() * 256f64;
let z_origin = random.next_f64() * 256f64;
let mut permutation = [0u8; 256];
permutation
.iter_mut()
.enumerate()
.for_each(|(i, x)| *x = i as u8);
for i in 0..256 {
let j = random.next_bounded_i32(256 - i) as usize;
permutation.swap(i as usize, i as usize + j);
}
Self {
permutation: Box::new(permutation),
x_origin,
y_origin,
z_origin,
}
}
fn map(&self, input: i32) -> i32 {
self.permutation[(input & 0xFF) as usize] as i32
}
fn grad(gradient_index: usize, x: f64, y: f64, z: f64, distance: f64) -> f64 {
let d = distance - x * x - y * y - z * z;
if d < 0f64 {
0f64
} else {
let d = d * d;
d * d * dot(&GRADIENTS[gradient_index], x, y, z)
}
}
pub fn sample_2d(&self, x: f64, y: f64) -> f64 {
let d = (x + y) * Self::SKEW_FACTOR_2D;
let i = (x + d).floor() as i32;
let j = (y + d).floor() as i32;
let e = (i.wrapping_add(j)) as f64 * Self::UNSKEW_FACTOR_2D;
let f = i as f64 - e;
let g = j as f64 - e;
let h = x - f;
let k = y - g;
let (l, m) = if h > k { (1, 0) } else { (0, 1) };
let n = h - l as f64 + Self::UNSKEW_FACTOR_2D;
let o = k - m as f64 + Self::UNSKEW_FACTOR_2D;
let p = h - 1f64 + 2f64 * Self::UNSKEW_FACTOR_2D;
let q = k - 1f64 + 2f64 * Self::UNSKEW_FACTOR_2D;
let r = i & 0xFF;
let s = j & 0xFF;
let t = self.map(r.wrapping_add(self.map(s))) % 12;
let u = self.map(r.wrapping_add(l).wrapping_add(self.map(s.wrapping_add(m)))) % 12;
let v = self.map(r.wrapping_add(1).wrapping_add(self.map(s.wrapping_add(1)))) % 12;
let w = Self::grad(t as usize, h, k, 0f64, 0.5f64);
let z = Self::grad(u as usize, n, o, 0f64, 0.5f64);
let aa = Self::grad(v as usize, p, q, 0f64, 0.5f64);
70f64 * (w + z + aa)
}
pub fn sample_3d(&self, x: f64, y: f64, z: f64) -> f64 {
let e = (x + y + z) * 0.3333333333333333f64;
let i = (x + e).floor() as i32;
let j = (y + e).floor() as i32;
let k = (z + e).floor() as i32;
let g = (i.wrapping_add(j).wrapping_add(k)) as f64 * 0.16666666666666666f64;
let h = i as f64 - g;
let l = j as f64 - g;
let m = k as f64 - g;
let n = x - h;
let o = y - l;
let p = z - m;
let (q, r, s, t, u, v) = if n >= o {
if o >= p {
(1, 0, 0, 1, 1, 0)
} else if n >= p {
(1, 0, 0, 1, 0, 1)
} else {
(0, 0, 1, 1, 0, 1)
}
} else if o < p {
(0, 0, 1, 0, 1, 1)
} else if n < p {
(0, 1, 0, 0, 1, 1)
} else {
(0, 1, 0, 1, 1, 0)
};
let w = n - q as f64 + 0.16666666666666666f64;
let aa = o - r as f64 + 0.16666666666666666f64;
let ab = p - s as f64 + 0.16666666666666666f64;
let ac = n - t as f64 + 0.3333333333333333f64;
let ad = o - u as f64 + 0.3333333333333333f64;
let ae = p - v as f64 + 0.3333333333333333f64;
let af = n - 1f64 + 0.5f64;
let ag = o - 1f64 + 0.5f64;
let ah = p - 1f64 + 0.5f64;
let ai = i & 0xFF;
let aj = j & 0xFF;
let ak = k & 0xFF;
let al = self.map(ai.wrapping_add(self.map(aj.wrapping_add(self.map(ak))))) % 12;
let am = self.map(
ai.wrapping_add(q).wrapping_add(
self.map(
aj.wrapping_add(r)
.wrapping_add(self.map(ak.wrapping_add(s))),
),
),
) % 12;
let an = self.map(
ai.wrapping_add(t).wrapping_add(
self.map(
aj.wrapping_add(u)
.wrapping_add(self.map(ak.wrapping_add(v))),
),
),
) % 12;
let ao = self.map(
ai.wrapping_add(1).wrapping_add(
self.map(
aj.wrapping_add(1)
.wrapping_add(self.map(ak.wrapping_add(1))),
),
),
) % 12;
let ap = Self::grad(al as usize, n, o, p, 0.6f64);
let aq = Self::grad(am as usize, w, aa, ab, 0.6f64);
let ar = Self::grad(an as usize, ac, ad, ae, 0.6f64);
let az = Self::grad(ao as usize, af, ag, ah, 0.6f64);
32f64 * (ap + aq + ar + az)
}
}
pub struct OctaveSimplexNoiseSampler {
octave_samplers: Vec<Option<SimplexNoiseSampler>>,
persistence: f64,
lacunarity: f64,
}
impl OctaveSimplexNoiseSampler {
pub fn new(random: &mut impl RandomImpl, octaves: &[i32]) -> Self {
let mut octaves = Vec::from_iter(octaves);
octaves.sort();
let i = -**octaves.first().expect("Should have some octaves");
let j = **octaves.last().expect("Should have some octaves");
let k = i.wrapping_add(j).wrapping_add(1);
let sampler = SimplexNoiseSampler::new(random);
let l = j;
let mut samplers: Vec<Option<SimplexNoiseSampler>> = Vec::with_capacity(k as usize);
for _ in 0..k {
samplers.push(None);
}
for m in (j + 1)..k {
if m >= 0 && octaves.contains(&&(l - m)) {
let sampler = SimplexNoiseSampler::new(random);
samplers[m as usize] = Some(sampler);
} else {
random.skip(262);
}
}
if j > 0 {
let sample = sampler.sample_3d(sampler.x_origin, sampler.y_origin, sampler.z_origin);
let n = (sample * 9.223372E18f32 as f64) as i64;
let mut random = LegacyRand::from_seed(n as u64);
for o in (0..=(l - 1)).rev() {
if o < k && octaves.contains(&&(l - o)) {
let sampler = SimplexNoiseSampler::new(&mut random);
samplers[o as usize] = Some(sampler);
} else {
random.skip(262);
}
}
}
if j >= 0 && j < k && octaves.contains(&&0) {
samplers[j as usize] = Some(sampler);
}
Self {
octave_samplers: samplers,
persistence: 1f64 / (2f64.pow(k) - 1f64),
lacunarity: 2f64.pow(j),
}
}
pub fn sample(&self, x: f64, y: f64, use_origin: bool) -> f64 {
let mut d = 0f64;
let mut e = self.lacunarity;
let mut f = self.persistence;
for sampler in self.octave_samplers.iter() {
if let Some(sampler) = sampler {
d += sampler.sample_2d(
x * e + if use_origin { sampler.x_origin } else { 0f64 },
y * e + if use_origin { sampler.y_origin } else { 0f64 },
) * f;
}
e /= 2f64;
f *= 2f64;
}
d
}
}
#[cfg(test)]
mod octave_simplex_noise_sampler_test {
use pumpkin_core::random::{xoroshiro128::Xoroshiro, RandomImpl};
use crate::world_gen::noise::simplex::OctaveSimplexNoiseSampler;
#[test]
fn test_new() {
let mut rand = Xoroshiro::from_seed(450);
assert_eq!(rand.next_i32(), 1394613419);
let sampler = OctaveSimplexNoiseSampler::new(&mut rand, &[-1, 1, 0]);
assert_eq!(sampler.lacunarity, 2f64);
assert_eq!(sampler.persistence, 0.14285714285714285);
let values = [
(33.48154133535127, 200.15584029786743, 239.82697852863149),
(115.65071632913913, 5.88805286077266, 184.4887403898897),
(64.69791492580848, 19.256055216755044, 97.01795462351956),
];
assert_eq!(values.len(), sampler.octave_samplers.len());
for (sampler, (x, y, z)) in sampler.octave_samplers.iter().zip(values) {
match sampler {
Some(sampler) => {
assert_eq!(sampler.x_origin, x);
assert_eq!(sampler.y_origin, y);
assert_eq!(sampler.z_origin, z);
}
None => panic!(),
}
}
}
#[test]
fn test_sample() {
let mut rand = Xoroshiro::from_seed(450);
assert_eq!(rand.next_i32(), 1394613419);
let sampler = OctaveSimplexNoiseSampler::new(&mut rand, &[-1, 1, 0]);
let values_1 = [
(
(-1.3127900550351206E7, 792897.4979227383),
-0.4321152413690901,
),
(
(-1.6920637874404985E7, -2.7155569346339065E8),
-0.5262902093081003,
),
(
(4.3144247722741723E8, 5.681942883881191E8),
0.11591369897395602,
),
(
(1.4302738270336467E8, -1.4548998886244193E8),
-0.3879951077548365,
),
(
(-3.9028350711219925E8, -5.213995559811158E7),
-0.7540785159288218,
),
(
(-1.3442750163759476E8, -6.725465365393716E8),
0.31442035977402105,
),
(
(-1.1937282161424601E8, 3.2134650034986335E8),
0.28218849676360336,
),
(
(-3.128475507865152E8, -3.014112871163455E8),
0.593770404657594,
),
(
(1.2027011883589141E8, -5.045175636913682E8),
-0.2893240282016911,
),
(
(-9.065155753781198E7, 6106991.342893547),
-0.3402301205344082,
),
];
for ((x, y), sample) in values_1 {
assert_eq!(sampler.sample(x, y, false), sample);
}
let values_2 = [
(
(-1.3127900550351206E7, 792897.4979227383),
0.21834818545873672,
),
(
(-1.6920637874404985E7, -2.7155569346339065E8),
0.025042742676442978,
),
(
(4.3144247722741723E8, 5.681942883881191E8),
0.3738693783591451,
),
(
(1.4302738270336467E8, -1.4548998886244193E8),
-0.023113657524218345,
),
(
(-3.9028350711219925E8, -5.213995559811158E7),
0.5195582376240916,
),
(
(-1.3442750163759476E8, -6.725465365393716E8),
0.020366186088347903,
),
(
(-1.1937282161424601E8, 3.2134650034986335E8),
-0.10921072611129382,
),
(
(-3.128475507865152E8, -3.014112871163455E8),
0.18066933648141983,
),
(
(1.2027011883589141E8, -5.045175636913682E8),
-0.36788084946294336,
),
(
(-9.065155753781198E7, 6106991.342893547),
-0.5677921377363926,
),
];
for ((x, y), sample) in values_2 {
assert_eq!(sampler.sample(x, y, true), sample);
}
}
}
#[cfg(test)]
mod simplex_noise_sampler_test {
use std::ops::Deref;
use pumpkin_core::random::{xoroshiro128::Xoroshiro, RandomImpl};
use crate::world_gen::noise::simplex::SimplexNoiseSampler;
#[test]
fn test_create() {
let mut rand = Xoroshiro::from_seed(111);
assert_eq!(rand.next_i32(), -1467508761);
let sampler = SimplexNoiseSampler::new(&mut rand);
assert_eq!(sampler.x_origin, 48.58072036717974f64);
assert_eq!(sampler.y_origin, 110.73235882678037f64);
assert_eq!(sampler.z_origin, 65.26438852860176f64);
let permutation: [u8; 256] = [
159, 113, 41, 143, 203, 123, 95, 177, 25, 79, 229, 219, 194, 60, 130, 14, 83, 99, 24,
202, 207, 232, 167, 152, 220, 201, 29, 235, 87, 147, 74, 160, 155, 97, 111, 31, 85,
205, 115, 50, 13, 171, 77, 237, 149, 116, 209, 174, 169, 109, 221, 9, 166, 84, 54, 216,
121, 106, 211, 16, 69, 244, 65, 192, 183, 146, 124, 37, 56, 45, 193, 158, 126, 217, 36,
255, 162, 163, 230, 103, 63, 90, 191, 214, 20, 138, 32, 39, 238, 67, 64, 105, 250, 140,
148, 114, 68, 75, 200, 161, 239, 125, 227, 199, 101, 61, 175, 107, 129, 240, 170, 51,
139, 86, 186, 145, 212, 178, 30, 251, 89, 226, 120, 153, 47, 141, 233, 2, 179, 236, 1,
19, 98, 21, 164, 108, 11, 23, 91, 204, 119, 88, 165, 195, 168, 26, 48, 206, 128, 6, 52,
118, 110, 180, 197, 231, 117, 7, 3, 135, 224, 58, 82, 78, 4, 59, 222, 18, 72, 57, 150,
43, 246, 100, 122, 112, 53, 133, 93, 17, 27, 210, 142, 234, 245, 80, 22, 46, 185, 172,
71, 248, 33, 173, 76, 35, 40, 92, 228, 127, 254, 70, 42, 208, 73, 104, 187, 62, 154,
243, 189, 241, 34, 66, 249, 94, 8, 12, 134, 132, 102, 242, 196, 218, 181, 28, 38, 15,
151, 157, 247, 223, 198, 55, 188, 96, 0, 182, 49, 190, 156, 10, 215, 252, 131, 137,
184, 176, 136, 81, 44, 213, 253, 144, 225, 5,
];
assert_eq!(sampler.permutation.deref(), permutation);
}
#[test]
fn test_sample_2d() {
let data1 = [
((-50000, 0), -0.013008608535752102),
((-49999, 1000), 0.0),
((-49998, 2000), -0.03787856584046271),
((-49997, 3000), 0.0),
((-49996, 4000), 0.5015373706471664),
((-49995, 5000), -0.032797908620906514),
((-49994, 6000), -0.19158655563621785),
((-49993, 7000), 0.49893473629544977),
((-49992, 8000), 0.31585737840402556),
((-49991, 9000), 0.43909577227435836),
];
let data2 = [
(
(-3.134738528791615E8, 5.676610095659718E7),
0.018940199193618792,
),
(
(-1369026.560586418, 3.957311252810864E8),
-0.1417598930091471,
),
(
(6.439373693833767E8, -3.36218773041759E8),
0.07129176668335062,
),
(
(1.353820060118252E8, -3.204701624793043E8),
0.330648835988156,
),
(
(-6906850.625560562, 1.0153663948838013E8),
0.46826928755778685,
),
(
(-7.108376621385525E7, -2.029413580824217E8),
-0.515950097501492,
),
(
(1.0591429119126628E8, -4.7911044364543396E8),
-0.5467822192664874,
),
(
(4.04615501401398E7, -3.074409286586152E8),
0.7470460844090322,
),
(
(-4.8645283544246924E8, -3.922570151180015E8),
0.8521699147242563,
),
(
(2.861710031285905E8, -1.8973201372718483E8),
0.1889297962671115,
),
(
(2.885407603819252E8, -3.358708100884505E7),
0.24006029504945695,
),
(
(3.6548491156354237E8, 7.995429702025633E7),
-0.8114171447379924,
),
(
(1.3298684552869435E8, 3.6743804723880893E8),
0.07042306408164949,
),
(
(-1.3123184148036437E8, -2.722300890805201E8),
0.5093850689193259,
),
(
(-5.56047682304707E8, 3.554803693060646E8),
-0.6343788467687929,
),
(
(5.638216625134594E8, -2.236907346192737E8),
0.5848746152449286,
),
(
(-5.436956979127073E7, -1.129261611506945E8),
-0.05456282199582522,
),
(
(1.0915760091641709E8, 1.932642099859593E7),
-0.273739377096594,
),
(
(-6.73911758014991E8, -2.2147483413687566E8),
0.05464681163741797,
),
(
(-2.4827386778136212E8, -2.6640208832089204E8),
-0.0902449424742273,
),
];
let mut rand = Xoroshiro::from_seed(111);
assert_eq!(rand.next_i32(), -1467508761);
let sampler = SimplexNoiseSampler::new(&mut rand);
for ((x, y), sample) in data1 {
assert_eq!(sampler.sample_2d(x as f64, y as f64), sample);
}
for ((x, y), sample) in data2 {
assert_eq!(sampler.sample_2d(x, y), sample);
}
}
#[test]
fn test_sample_3d() {
let data = [
(
(
-3.134738528791615E8,
5.676610095659718E7,
2.011711832498507E8,
),
-0.07626353895981935,
),
(
(-1369026.560586418, 3.957311252810864E8, 6.797037355570006E8),
0.0,
),
(
(
6.439373693833767E8,
-3.36218773041759E8,
-3.265494249695775E8,
),
-0.5919400355725402,
),
(
(
1.353820060118252E8,
-3.204701624793043E8,
-4.612474746056331E8,
),
-0.5220477236433517,
),
(
(
-6906850.625560562,
1.0153663948838013E8,
2.4923185478305575E8,
),
-0.39146687767898636,
),
(
(
-7.108376621385525E7,
-2.029413580824217E8,
2.5164602748045415E8,
),
-0.629386846329711,
),
(
(
1.0591429119126628E8,
-4.7911044364543396E8,
-2918719.2277242197,
),
0.5427502531663232,
),
(
(
4.04615501401398E7,
-3.074409286586152E8,
5.089118769334092E7,
),
-0.4273080639878097,
),
(
(
-4.8645283544246924E8,
-3.922570151180015E8,
2.3741632952563038E8,
),
0.32129944093252394,
),
(
(
2.861710031285905E8,
-1.8973201372718483E8,
-3.2653143323982143E8,
),
0.35839032946039706,
),
(
(
2.885407603819252E8,
-3.358708100884505E7,
-1.4480399660676318E8,
),
-0.02451312935907038,
),
(
(
3.6548491156354237E8,
7.995429702025633E7,
2.509991661702412E8,
),
-0.36830526266318003,
),
(
(
1.3298684552869435E8,
3.6743804723880893E8,
5.791092458225288E7,
),
-0.023683302916542803,
),
(
(
-1.3123184148036437E8,
-2.722300890805201E8,
2.1601883778132245E7,
),
-0.261629562325043,
),
(
(
-5.56047682304707E8,
3.554803693060646E8,
3.1647392358159083E8,
),
-0.4959372930161496,
),
(
(
5.638216625134594E8,
-2.236907346192737E8,
-5.0562852022285646E8,
),
-0.06079315675880484,
),
(
(
-5.436956979127073E7,
-1.129261611506945E8,
-1.7909512156895646E8,
),
-0.37726907424345196,
),
(
(
1.0915760091641709E8,
1.932642099859593E7,
-3.405060533753616E8,
),
0.37747828159811136,
),
(
(
-6.73911758014991E8,
-2.2147483413687566E8,
-4.531457195005102E7,
),
-0.32929020207000603,
),
(
(
-2.4827386778136212E8,
-2.6640208832089204E8,
-3.354675096522197E8,
),
-0.3046390200444667,
),
];
let mut rand = Xoroshiro::from_seed(111);
assert_eq!(rand.next_i32(), -1467508761);
let sampler = SimplexNoiseSampler::new(&mut rand);
for ((x, y, z), sample) in data {
assert_eq!(sampler.sample_3d(x, y, z), sample);
}
}
}

View File

@@ -10,16 +10,16 @@ plugins = ["pumpkin-plugin/plugins"]
[dependencies]
# pumpkin
pumpkin-core = { path = "../pumpkin-core"}
pumpkin-core = { path = "../pumpkin-core" }
pumpkin-config = { path = "../pumpkin-config" }
pumpkin-plugin = { path = "../pumpkin-plugin"}
pumpkin-inventory = { path = "../pumpkin-inventory"}
pumpkin-world = { path = "../pumpkin-world"}
pumpkin-entity = { path = "../pumpkin-entity"}
pumpkin-protocol = { path = "../pumpkin-protocol"}
pumpkin-registry = { path = "../pumpkin-registry"}
pumpkin-plugin = { path = "../pumpkin-plugin" }
pumpkin-inventory = { path = "../pumpkin-inventory" }
pumpkin-world = { path = "../pumpkin-world" }
pumpkin-entity = { path = "../pumpkin-entity" }
pumpkin-protocol = { path = "../pumpkin-protocol" }
pumpkin-registry = { path = "../pumpkin-registry" }
itertools = "0.13.0"
itertools.workspace = true
# config
serde.workspace = true
@@ -40,7 +40,12 @@ rsa = "0.9.6"
rsa-der = "0.3.0"
# authentication
reqwest = { version = "0.12.7", default-features= false, features = ["json", "rustls-tls", "http2", "macos-system-configuration"]}
reqwest = { version = "0.12.7", default-features = false, features = [
"http2",
"json",
"macos-system-configuration",
"rustls-tls",
] }
sha1 = "0.10.6"
digest = "=0.11.0-pre.9"
@@ -53,15 +58,17 @@ thiserror = "1.0"
# icon loading
base64 = "0.22.1"
image = { version = "0.25", default-features = false, features = ["png"]}
png = "0.17.14"
# logging
simple_logger = "5.0.0"
simple_logger = { version = "5.0.0", features = ["threads"] }
log.workspace = true
# networking
mio = { version = "1.0.2", features = ["os-poll", "net"]}
mio = { version = "1.0.2", features = ["net", "os-poll"] }
parking_lot.workspace = true
crossbeam.workspace = true
uuid.workspace = true
tokio.workspace = true
rayon.workspace = true

View File

@@ -1,7 +1,6 @@
use std::{collections::HashMap, net::IpAddr, sync::Arc};
use base64::{engine::general_purpose, Engine};
use num_bigint::BigInt;
use pumpkin_config::{auth::TextureConfig, ADVANCED_CONFIG};
use pumpkin_core::ProfileAction;
use pumpkin_protocol::Property;
@@ -39,6 +38,19 @@ pub struct GameProfile {
pub profile_actions: Option<Vec<ProfileAction>>,
}
/// Sends a GET request to Mojang's authentication servers to verify a client's Minecraft account.
///
/// **Purpose:**
///
/// This function is used to ensure that a client connecting to the server has a valid, premium Minecraft account. It's a crucial step in preventing unauthorized access and maintaining server security.
///
/// **How it Works:**
///
/// 1. A client with a premium account sends a login request to the Mojang session server.
/// 2. Mojang's servers verify the client's credentials and add the player to the their Servers
/// 3. Now our server will send a Request to the Session servers and check if the Player has joined the Session Server .
///
/// **Note:** This process helps prevent unauthorized access to the server and ensures that only legitimate Minecraft accounts can connect.
pub async fn authenticate(
username: &str,
server_hash: &str,
@@ -48,9 +60,18 @@ pub async fn authenticate(
assert!(ADVANCED_CONFIG.authentication.enabled);
assert!(server.auth_client.is_some());
let address = if ADVANCED_CONFIG.authentication.prevent_proxy_connections {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}&ip={ip}")
ADVANCED_CONFIG
.authentication
.auth_url
.replace("{username}", username)
.replace("{server_hash}", server_hash)
.replace("{}", &ip.to_string())
} else {
format!("https://sessionserver.mojang.com/session/minecraft/hasJoined?username={username}&serverId={server_hash}")
ADVANCED_CONFIG
.authentication
.auth_url
.replace("{username}", username)
.replace("{server_hash}", server_hash)
};
let auth_client = server
.auth_client
@@ -65,35 +86,44 @@ pub async fn authenticate(
match response.status() {
StatusCode::OK => {}
StatusCode::NO_CONTENT => Err(AuthError::UnverifiedUsername)?,
other => Err(AuthError::UnknownStatusCode(other.as_str().to_string()))?,
other => Err(AuthError::UnknownStatusCode(other))?,
}
let profile: GameProfile = response.json().await.map_err(|_| AuthError::FailedParse)?;
Ok(profile)
}
pub fn unpack_textures(property: Property, config: &TextureConfig) {
// TODO: no unwrap
let from64 = general_purpose::STANDARD.decode(property.value).unwrap();
let textures: ProfileTextures = serde_json::from_slice(&from64).unwrap();
pub fn unpack_textures(property: &Property, config: &TextureConfig) -> Result<(), TextureError> {
let from64 = general_purpose::STANDARD
.decode(&property.value)
.map_err(|e| TextureError::DecodeError(e.to_string()))?;
let textures: ProfileTextures =
serde_json::from_slice(&from64).map_err(|e| TextureError::JSONError(e.to_string()))?;
for texture in textures.textures {
is_texture_url_valid(Url::parse(&texture.1.url).unwrap(), config);
let url =
Url::parse(&texture.1.url).map_err(|e| TextureError::InvalidURL(e.to_string()))?;
is_texture_url_valid(url, config)?
}
Ok(())
}
pub fn auth_digest(bytes: &[u8]) -> String {
BigInt::from_signed_bytes_be(bytes).to_str_radix(16)
}
pub fn is_texture_url_valid(url: Url, config: &TextureConfig) -> bool {
pub fn is_texture_url_valid(url: Url, config: &TextureConfig) -> Result<(), TextureError> {
let scheme = url.scheme();
if !config.allowed_url_schemes.contains(&scheme.to_string()) {
return false;
if !config
.allowed_url_schemes
.iter()
.any(|allowed_scheme| scheme.ends_with(allowed_scheme))
{
return Err(TextureError::DisallowedUrlScheme(scheme.to_string()));
}
let domain = url.domain().unwrap_or("");
if !config.allowed_url_domains.contains(&domain.to_string()) {
return false;
if !config
.allowed_url_domains
.iter()
.any(|allowed_domain| domain.ends_with(allowed_domain))
{
return Err(TextureError::DisallowedUrlDomain(domain.to_string()));
}
true
Ok(())
}
#[derive(Error, Debug)]
@@ -107,5 +137,19 @@ pub enum AuthError {
#[error("Failed to parse JSON into Game Profile")]
FailedParse,
#[error("Unknown Status Code")]
UnknownStatusCode(String),
UnknownStatusCode(StatusCode),
}
#[derive(Error, Debug)]
pub enum TextureError {
#[error("Invalid URL")]
InvalidURL(String),
#[error("Invalid URL scheme for player texture: {0}")]
DisallowedUrlScheme(String),
#[error("Invalid URL domain for player texture: {0}")]
DisallowedUrlDomain(String),
#[error("Failed to decode base64 player texture: {0}")]
DecodeError(String),
#[error("Failed to parse JSON from player texture: {0}")]
JSONError(String),
}

View File

@@ -6,8 +6,8 @@ use pumpkin_core::text::TextComponent;
use pumpkin_protocol::{
client::{
config::{CConfigAddResourcePack, CFinishConfig, CKnownPacks, CRegistryData},
login::{CEncryptionRequest, CLoginSuccess, CSetCompression},
status::{CPingResponse, CStatusResponse},
login::{CLoginSuccess, CSetCompression},
status::CPingResponse,
},
server::{
config::{SAcknowledgeFinishConfig, SClientInformationConfig, SKnownPacks, SPluginMessage},
@@ -17,8 +17,7 @@ use pumpkin_protocol::{
},
ConnectionState, KnownPack, CURRENT_MC_PROTOCOL,
};
use rsa::Pkcs1v15Encrypt;
use sha1::{Digest, Sha1};
use uuid::Uuid;
use crate::{
client::authentication::{self, GameProfile},
@@ -27,22 +26,22 @@ use crate::{
server::{Server, CURRENT_MC_VERSION},
};
use super::{
authentication::{auth_digest, unpack_textures},
Client, EncryptionError, PlayerConfig,
};
use super::{authentication::unpack_textures, Client, PlayerConfig};
/// Processes incoming Packets from the Client to the Server
/// Implements the `Client` Packets
/// NEVER TRUST THE CLIENT. HANDLE EVERY ERROR, UNWRAP/EXPECT
/// TODO: REMOVE ALL UNWRAPS
impl Client {
pub fn handle_handshake(&mut self, _server: &Arc<Server>, handshake: SHandShake) {
pub fn handle_handshake(&self, _server: &Arc<Server>, handshake: SHandShake) {
dbg!("handshake");
self.protocol_version = handshake.protocol_version.0;
self.connection_state = handshake.next_state;
if self.connection_state != ConnectionState::Status {
let protocol = self.protocol_version;
let version = handshake.protocol_version.0;
self.protocol_version
.store(version, std::sync::atomic::Ordering::Relaxed);
self.connection_state.store(handshake.next_state);
if self.connection_state.load() != ConnectionState::Status {
let protocol = version;
match protocol.cmp(&(CURRENT_MC_PROTOCOL as i32)) {
std::cmp::Ordering::Less => {
self.kick(&format!("Client outdated ({protocol}), Server uses Minecraft {CURRENT_MC_VERSION}, Protocol {CURRENT_MC_PROTOCOL}"));
@@ -55,11 +54,11 @@ impl Client {
}
}
pub fn handle_status_request(&mut self, server: &Arc<Server>, _status_request: SStatusRequest) {
self.send_packet(&CStatusResponse::new(&server.status_response_json));
pub fn handle_status_request(&self, server: &Arc<Server>, _status_request: SStatusRequest) {
self.send_packet(&server.get_status());
}
pub fn handle_ping_request(&mut self, _server: &Arc<Server>, ping_request: SStatusPingRequest) {
pub fn handle_ping_request(&self, _server: &Arc<Server>, ping_request: SStatusPingRequest) {
dbg!("ping");
self.send_packet(&CPingResponse::new(ping_request.payload));
self.close();
@@ -72,7 +71,7 @@ impl Client {
.all(|c| c > 32_u8 as char && c < 127_u8 as char)
}
pub fn handle_login_start(&mut self, server: &Arc<Server>, login_start: SLoginStart) {
pub fn handle_login_start(&self, server: &Arc<Server>, login_start: SLoginStart) {
log::debug!("login start, State {:?}", self.connection_state);
if !Self::is_valid_player_name(&login_start.name) {
@@ -81,7 +80,8 @@ impl Client {
}
// default game profile, when no online mode
// TODO: make offline uuid
self.gameprofile = Some(GameProfile {
let mut gameprofile = self.gameprofile.lock();
*gameprofile = Some(GameProfile {
id: login_start.uuid,
name: login_start.name,
properties: vec![],
@@ -97,87 +97,74 @@ impl Client {
// TODO: check config for encryption
let verify_token: [u8; 4] = rand::random();
let public_key_der = &server.public_key_der;
let packet = CEncryptionRequest::new(
"",
public_key_der,
&verify_token,
BASIC_CONFIG.online_mode, // TODO
);
self.send_packet(&packet);
self.send_packet(&server.encryption_request(&verify_token, BASIC_CONFIG.online_mode));
}
pub async fn handle_encryption_response(
&mut self,
&self,
server: &Arc<Server>,
encryption_response: SEncryptionResponse,
) {
let shared_secret = server
.private_key
.decrypt(Pkcs1v15Encrypt, &encryption_response.shared_secret)
.map_err(|_| EncryptionError::FailedDecrypt)
.unwrap();
self.enable_encryption(&shared_secret)
let shared_secret = server.decrypt(&encryption_response.shared_secret).unwrap();
self.set_encryption(Some(&shared_secret))
.unwrap_or_else(|e| self.kick(&e.to_string()));
let mut gameprofile = self.gameprofile.lock();
if BASIC_CONFIG.online_mode {
let hash = Sha1::new()
.chain_update(&shared_secret)
.chain_update(&server.public_key_der)
.finalize();
let hash = auth_digest(&hash);
let ip = self.address.ip();
let hash = server.digest_secret(&shared_secret);
let ip = self.address.lock().ip();
match authentication::authenticate(
&self.gameprofile.as_ref().unwrap().name,
&gameprofile.as_ref().unwrap().name,
&hash,
&ip,
server,
)
.await
{
Ok(p) => {
Ok(profile) => {
// Check if player should join
if let Some(p) = &p.profile_actions {
if let Some(actions) = &profile.profile_actions {
if !ADVANCED_CONFIG
.authentication
.player_profile
.allow_banned_players
{
if !p.is_empty() {
if !actions.is_empty() {
self.kick("Your account can't join");
}
} else {
for allowed in ADVANCED_CONFIG
for allowed in &ADVANCED_CONFIG
.authentication
.player_profile
.allowed_actions
.clone()
{
if !p.contains(&allowed) {
if !actions.contains(allowed) {
self.kick("Your account can't join");
}
}
}
}
self.gameprofile = Some(p);
*gameprofile = Some(profile);
}
Err(e) => self.kick(&e.to_string()),
}
}
for ele in self.gameprofile.as_ref().unwrap().properties.clone() {
// todo, use this
unpack_textures(ele, &ADVANCED_CONFIG.authentication.textures);
for property in &gameprofile.as_ref().unwrap().properties {
unpack_textures(property, &ADVANCED_CONFIG.authentication.textures)
.unwrap_or_else(|e| self.kick(&e.to_string()));
}
// enable compression
if ADVANCED_CONFIG.packet_compression.enabled {
let threshold = ADVANCED_CONFIG.packet_compression.compression_threshold;
let level = ADVANCED_CONFIG.packet_compression.compression_level;
self.send_packet(&CSetCompression::new(threshold.into()));
self.set_compression(Some((threshold, level)));
let compression = ADVANCED_CONFIG.packet_compression.compression_info.clone();
self.send_packet(&CSetCompression::new(compression.threshold.into()));
self.set_compression(Some(compression));
}
if let Some(profile) = self.gameprofile.as_ref().cloned() {
if let Some(profile) = gameprofile.as_ref() {
let packet = CLoginSuccess::new(&profile.id, &profile.name, &profile.properties, false);
self.send_packet(&packet);
} else {
@@ -186,37 +173,38 @@ impl Client {
}
pub fn handle_plugin_response(
&mut self,
&self,
_server: &Arc<Server>,
_plugin_response: SLoginPluginResponse,
) {
}
pub fn handle_login_acknowledged(
&mut self,
&self,
server: &Arc<Server>,
_login_acknowledged: SLoginAcknowledged,
) {
self.connection_state = ConnectionState::Config;
server.send_brand(self);
self.connection_state.store(ConnectionState::Config);
self.send_packet(&server.get_branding());
let resource_config = &ADVANCED_CONFIG.resource_pack;
if resource_config.enabled {
let prompt_message = if resource_config.prompt_message.is_empty() {
None
} else {
Some(TextComponent::text(&resource_config.prompt_message))
};
self.send_packet(&CConfigAddResourcePack::new(
pumpkin_protocol::uuid::UUID(uuid::Uuid::new_v3(
let resource_pack = CConfigAddResourcePack::new(
Uuid::new_v3(
&uuid::Uuid::NAMESPACE_DNS,
resource_config.resource_pack_url.as_bytes(),
)),
),
&resource_config.resource_pack_url,
&resource_config.resource_pack_sha1,
resource_config.force,
prompt_message,
));
if !resource_config.prompt_message.is_empty() {
Some(TextComponent::text(&resource_config.prompt_message))
} else {
None
},
);
self.send_packet(&resource_pack);
}
// known data packs
@@ -225,39 +213,46 @@ impl Client {
id: "core",
version: "1.21",
}]));
dbg!("login achnowlaged");
dbg!("login acknowledged");
}
pub fn handle_client_information_config(
&mut self,
&self,
_server: &Arc<Server>,
client_information: SClientInformationConfig,
) {
dbg!("got client settings");
self.config = Some(PlayerConfig {
locale: client_information.locale,
view_distance: client_information.view_distance,
chat_mode: ChatMode::from_i32(client_information.chat_mode.into()).unwrap(),
chat_colors: client_information.chat_colors,
skin_parts: client_information.skin_parts,
main_hand: Hand::from_i32(client_information.main_hand.into()).unwrap(),
text_filtering: client_information.text_filtering,
server_listing: client_information.server_listing,
});
if let (Some(main_hand), Some(chat_mode)) = (
Hand::from_i32(client_information.main_hand.into()),
ChatMode::from_i32(client_information.chat_mode.into()),
) {
*self.config.lock() = Some(PlayerConfig {
locale: client_information.locale,
view_distance: client_information.view_distance,
chat_mode,
chat_colors: client_information.chat_colors,
skin_parts: client_information.skin_parts,
main_hand,
text_filtering: client_information.text_filtering,
server_listing: client_information.server_listing,
});
} else {
self.kick("Invalid hand or chat type")
}
}
pub fn handle_plugin_message(&mut self, _server: &Arc<Server>, plugin_message: SPluginMessage) {
pub fn handle_plugin_message(&self, _server: &Arc<Server>, plugin_message: SPluginMessage) {
if plugin_message.channel.starts_with("minecraft:brand")
|| plugin_message.channel.starts_with("MC|Brand")
{
dbg!("got a client brand");
match String::from_utf8(plugin_message.data) {
Ok(brand) => self.brand = Some(brand),
Ok(brand) => *self.brand.lock() = Some(brand),
Err(e) => self.kick(&e.to_string()),
}
}
}
pub fn handle_known_packs(&mut self, server: &Arc<Server>, _config_acknowledged: SKnownPacks) {
pub fn handle_known_packs(&self, server: &Arc<Server>, _config_acknowledged: SKnownPacks) {
for registry in &server.cached_registry {
self.send_packet(&CRegistryData::new(
&registry.registry_id,
@@ -271,12 +266,13 @@ impl Client {
}
pub async fn handle_config_acknowledged(
&mut self,
&self,
_server: &Arc<Server>,
_config_acknowledged: SAcknowledgeFinishConfig,
) {
dbg!("config acknowledged");
self.connection_state = ConnectionState::Play;
self.make_player = true;
self.connection_state.store(ConnectionState::Play);
self.make_player
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}

View File

@@ -1,6 +1,7 @@
use crate::entity::player::Player;
use crate::server::Server;
use itertools::Itertools;
use parking_lot::Mutex;
use pumpkin_core::text::TextComponent;
use pumpkin_core::GameMode;
use pumpkin_inventory::container_click::{
@@ -16,16 +17,17 @@ use pumpkin_protocol::client::play::{
use pumpkin_protocol::server::play::SClickContainer;
use pumpkin_protocol::slot::Slot;
use pumpkin_world::item::ItemStack;
use std::sync::{Arc, Mutex};
use std::sync::Arc;
impl Player {
pub fn open_container(&mut self, server: &Arc<Server>, minecraft_menu_id: &str) {
self.inventory.state_id = 0;
let total_opened_containers = self.inventory.total_opened_containers;
pub fn open_container(&self, server: &Arc<Server>, minecraft_menu_id: &str) {
let inventory = self.inventory.lock();
inventory
.state_id
.store(0, std::sync::atomic::Ordering::Relaxed);
let total_opened_containers = inventory.total_opened_containers;
let container = self.get_open_container(server);
let mut container = container
.as_ref()
.map(|container| container.lock().unwrap());
let mut container = container.as_ref().map(|container| container.lock());
let menu_protocol_id = (*pumpkin_world::global_registry::REGISTRY
.get("minecraft:menu")
.unwrap()
@@ -38,7 +40,7 @@ impl Player {
let window_title = container
.as_ref()
.map(|container| container.window_name())
.unwrap_or(self.inventory.window_name());
.unwrap_or_else(|| inventory.window_name());
let title = TextComponent::text(window_title);
self.client.send_packet(&COpenScreen::new(
@@ -46,12 +48,15 @@ impl Player {
menu_protocol_id,
title,
));
drop(inventory);
self.set_container_content(container.as_deref_mut());
}
pub fn set_container_content(&mut self, container: Option<&mut Box<dyn Container>>) {
let total_opened_containers = self.inventory.total_opened_containers;
let container = OptionallyCombinedContainer::new(&mut self.inventory, container);
pub fn set_container_content(&self, container: Option<&mut Box<dyn Container>>) {
let mut inventory = self.inventory.lock();
let total_opened_containers = inventory.total_opened_containers;
let container = OptionallyCombinedContainer::new(&mut inventory, container);
let slots = container
.all_slots_ref()
@@ -59,17 +64,19 @@ impl Player {
.map(Slot::from)
.collect_vec();
let carried_item = {
if let Some(item) = self.carried_item.as_ref() {
item.into()
} else {
Slot::empty()
}
};
self.inventory.state_id += 1;
let carried_item = self
.carried_item
.load()
.as_ref()
.map_or_else(Slot::empty, |item| item.into());
// Gets the previous value
let i = inventory
.state_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let packet = CSetContainerContent::new(
total_opened_containers,
(self.inventory.state_id as i32).into(),
((i + 1) as i32).into(),
&slots,
&carried_item,
);
@@ -77,11 +84,11 @@ impl Player {
}
/// The official Minecraft client is weird, and will always just close *any* window that is opened when this gets sent
pub fn close_container(&mut self) {
self.inventory.total_opened_containers += 1;
self.client.send_packet(&CCloseContainer::new(
self.inventory.total_opened_containers,
))
pub fn close_container(&self) {
let mut inventory = self.inventory.lock();
inventory.total_opened_containers += 1;
self.client
.send_packet(&CCloseContainer::new(inventory.total_opened_containers))
}
pub fn set_container_property<T: WindowPropertyTrait>(
@@ -90,24 +97,26 @@ impl Player {
) {
let (id, value) = window_property.into_tuple();
self.client.send_packet(&CSetContainerProperty::new(
self.inventory.total_opened_containers,
self.inventory.lock().total_opened_containers,
id,
value,
));
}
pub async fn handle_click_container(
&mut self,
&self,
server: &Arc<Server>,
packet: SClickContainer,
) -> Result<(), InventoryError> {
let opened_container = self.get_open_container(server);
let mut opened_container = opened_container
.as_ref()
.map(|container| container.lock().unwrap());
let mut opened_container = opened_container.as_ref().map(|container| container.lock());
let drag_handler = &server.drag_handler;
let state_id = self.inventory.state_id;
let state_id = self
.inventory
.lock()
.state_id
.load(std::sync::atomic::Ordering::Relaxed);
// This is just checking for regular desync, client hasn't done anything malicious
if state_id != packet.state_id.0 as u32 {
self.set_container_content(opened_container.as_deref_mut());
@@ -115,7 +124,7 @@ impl Player {
}
if opened_container.is_some() {
if packet.window_id != self.inventory.total_opened_containers {
if packet.window_id != self.inventory.lock().total_opened_containers {
return Err(InventoryError::ClosedContainerInteract(self.entity_id()));
}
} else if packet.window_id != 0 {
@@ -177,10 +186,9 @@ impl Player {
drop(opened_container);
self.send_whole_container_change(server).await?;
} else if let container_click::Slot::Normal(slot_index) = click.slot {
let combined_container = OptionallyCombinedContainer::new(
&mut self.inventory,
Some(&mut opened_container),
);
let mut inventory = self.inventory.lock();
let combined_container =
OptionallyCombinedContainer::new(&mut inventory, Some(&mut opened_container));
if let Some(slot) = combined_container.get_slot_excluding_inventory(slot_index) {
let slot = Slot::from(slot);
drop(opened_container);
@@ -193,27 +201,32 @@ impl Player {
}
fn mouse_click(
&mut self,
&self,
opened_container: Option<&mut Box<dyn Container>>,
mouse_click: MouseClick,
slot: container_click::Slot,
) -> Result<(), InventoryError> {
let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
let mut inventory = self.inventory.lock();
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
match slot {
container_click::Slot::Normal(slot) => {
container.handle_item_change(&mut self.carried_item, slot, mouse_click)
let mut carried_item = self.carried_item.load();
let res = container.handle_item_change(&mut carried_item, slot, mouse_click);
self.carried_item.store(carried_item);
res
}
container_click::Slot::OutsideInventory => Ok(()),
}
}
fn shift_mouse_click(
&mut self,
&self,
opened_container: Option<&mut Box<dyn Container>>,
slot: container_click::Slot,
) -> Result<(), InventoryError> {
let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
let mut inventory = self.inventory.lock();
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
match slot {
container_click::Slot::Normal(slot) => {
@@ -255,7 +268,7 @@ impl Player {
}
fn number_button_pressed(
&mut self,
&self,
opened_container: Option<&mut Box<dyn Container>>,
key_click: KeyClick,
slot: usize,
@@ -264,35 +277,38 @@ impl Player {
KeyClick::Slot(slot) => slot,
KeyClick::Offhand => 45,
};
let mut changing_item_slot = self.inventory.get_slot(changing_slot as usize)?.to_owned();
let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
let mut inventory = self.inventory.lock();
let mut changing_item_slot = inventory.get_slot(changing_slot as usize)?.to_owned();
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
container.handle_item_change(&mut changing_item_slot, slot, MouseClick::Left)?;
*self.inventory.get_slot(changing_slot as usize)? = changing_item_slot;
*inventory.get_slot(changing_slot as usize)? = changing_item_slot;
Ok(())
}
fn creative_pick_item(
&mut self,
&self,
opened_container: Option<&mut Box<dyn Container>>,
slot: usize,
) -> Result<(), InventoryError> {
if self.gamemode != GameMode::Creative {
if self.gamemode.load() != GameMode::Creative {
return Err(InventoryError::PermissionError);
}
let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
let mut inventory = self.inventory.lock();
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
if let Some(Some(item)) = container.all_slots().get_mut(slot) {
self.carried_item = Some(item.to_owned())
self.carried_item.store(Some(item.to_owned()));
}
Ok(())
}
fn double_click(
&mut self,
&self,
opened_container: Option<&mut Box<dyn Container>>,
slot: usize,
) -> Result<(), InventoryError> {
let mut container = OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
let mut inventory = self.inventory.lock();
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
let mut slots = container.all_slots();
let Some(item) = slots.get_mut(slot) else {
@@ -320,12 +336,12 @@ impl Player {
}
}
}
self.carried_item = Some(carried_item);
self.carried_item.store(Some(carried_item));
Ok(())
}
fn mouse_drag(
&mut self,
&self,
drag_handler: &DragHandler,
opened_container: Option<&mut Box<dyn Container>>,
mouse_drag_state: MouseDragState,
@@ -337,36 +353,35 @@ impl Player {
.unwrap_or(player_id as u64);
match mouse_drag_state {
MouseDragState::Start(drag_type) => {
if drag_type == MouseDragType::Middle && self.gamemode != GameMode::Creative {
if drag_type == MouseDragType::Middle && self.gamemode.load() != GameMode::Creative
{
Err(InventoryError::PermissionError)?
}
drag_handler.new_drag(container_id, player_id, drag_type)
}
MouseDragState::AddSlot(slot) => drag_handler.add_slot(container_id, player_id, slot),
MouseDragState::End => {
let mut inventory = self.inventory.lock();
let mut container =
OptionallyCombinedContainer::new(&mut self.inventory, opened_container);
drag_handler.apply_drag(
&mut self.carried_item,
OptionallyCombinedContainer::new(&mut inventory, opened_container);
let mut carried_item = self.carried_item.load();
let res = drag_handler.apply_drag(
&mut carried_item,
&mut container,
&container_id,
player_id,
)
);
self.carried_item.store(carried_item);
res
}
}
}
async fn get_current_players_in_container(
&mut self,
server: &Server,
) -> Vec<Arc<Mutex<Player>>> {
async fn get_current_players_in_container(&self, server: &Server) -> Vec<Arc<Self>> {
let player_ids = {
let open_containers = server
.open_containers
.read()
.expect("open_containers is poisoned");
let open_containers = server.open_containers.read();
open_containers
.get(&self.open_container.unwrap())
.get(&self.open_container.load().unwrap())
.unwrap()
.all_player_ids()
.into_iter()
@@ -378,13 +393,16 @@ impl Player {
// TODO: Figure out better way to get only the players from player_ids
// Also refactor out a better method to get individual advanced state ids
let world = self.entity.world.lock().await;
let players = world
let players = self
.living_entity
.entity
.world
.current_players
.lock()
.iter()
.filter_map(|(token, player)| {
if *token != player_token {
let entity_id = player.lock().unwrap().entity_id();
let entity_id = player.entity_id();
if player_ids.contains(&entity_id) {
Some(player.clone())
} else {
@@ -399,19 +417,22 @@ impl Player {
}
async fn send_container_changes(
&mut self,
&self,
server: &Server,
slot_index: usize,
slot: Slot,
) -> Result<(), InventoryError> {
for player in self.get_current_players_in_container(server).await {
let mut player = player.lock().unwrap();
let total_opened_containers = player.inventory.total_opened_containers;
let inventory = player.inventory.lock();
let total_opened_containers = inventory.total_opened_containers;
player.inventory.state_id += 1;
// Returns previous value
let i = inventory
.state_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let packet = CSetContainerSlot::new(
total_opened_containers as i8,
player.inventory.state_id as i32,
(i + 1) as i32,
slot_index,
&slot,
);
@@ -420,23 +441,20 @@ impl Player {
Ok(())
}
async fn send_whole_container_change(&mut self, server: &Server) -> Result<(), InventoryError> {
async fn send_whole_container_change(&self, server: &Server) -> Result<(), InventoryError> {
let players = self.get_current_players_in_container(server).await;
for player in players {
let mut player = player.lock().unwrap();
let container = player.get_open_container(server);
let mut container = container.as_ref().map(|v| v.lock().unwrap());
let mut container = container.as_ref().map(|v| v.lock());
player.set_container_content(container.as_deref_mut());
}
Ok(())
}
pub fn get_open_container(&self, server: &Server) -> Option<Arc<Mutex<Box<dyn Container>>>> {
if let Some(id) = self.open_container {
server.try_get_container(self.entity_id(), id)
} else {
None
}
self.open_container
.load()
.map_or_else(|| None, |id| server.try_get_container(self.entity_id(), id))
}
}

View File

@@ -1,7 +1,10 @@
use std::{
io::{self, Write},
net::SocketAddr,
sync::Arc,
sync::{
atomic::{AtomicBool, AtomicI32},
Arc,
},
};
use crate::{
@@ -10,7 +13,10 @@ use crate::{
};
use authentication::GameProfile;
use crossbeam::atomic::AtomicCell;
use mio::{event::Event, net::TcpStream, Token};
use parking_lot::Mutex;
use pumpkin_config::compression::CompressionInfo;
use pumpkin_core::text::TextComponent;
use pumpkin_protocol::{
bytebuf::{packet_id::Packet, DeserializerError},
@@ -34,15 +40,30 @@ mod client_packet;
mod container;
pub mod player_packet;
/// Represents a player's configuration settings.
///
/// This struct contains various options that can be customized by the player, affecting their gameplay experience.
///
/// **Usage:**
///
/// This struct is typically used to store and manage a player's preferences. It can be sent to the server when a player joins or when they change their settings.
#[derive(Clone)]
pub struct PlayerConfig {
/// The player's preferred language.
pub locale: String, // 16
/// The maximum distance at which chunks are rendered.
pub view_distance: i8,
/// The player's chat mode settings
pub chat_mode: ChatMode,
/// Whether chat colors are enabled.
pub chat_colors: bool,
/// The player's skin configuration options.
pub skin_parts: u8,
/// The player's dominant hand (left or right).
pub main_hand: Hand,
/// Whether text filtering is enabled.
pub text_filtering: bool,
/// Whether the player wants to appear in the server list.
pub server_listing: bool,
}
@@ -61,206 +82,158 @@ impl Default for PlayerConfig {
}
}
/// Everything which makes a Connection with our Server is a `Client`.
/// Client will become Players when they reach the `Play` state
pub struct Client {
pub gameprofile: Option<GameProfile>,
pub config: Option<PlayerConfig>,
pub brand: Option<String>,
pub protocol_version: i32,
pub connection_state: ConnectionState,
pub encryption: bool,
pub closed: bool,
/// The client's game profile information.
pub gameprofile: Mutex<Option<GameProfile>>,
/// The client's configuration settings, Optional
pub config: Mutex<Option<PlayerConfig>>,
/// The client's brand or modpack information, Optional.
pub brand: Mutex<Option<String>>,
/// The minecraft protocol version used by the client.
pub protocol_version: AtomicI32,
/// The current connection state of the client (e.g., Handshaking, Status, Play).
pub connection_state: AtomicCell<ConnectionState>,
/// Whether encryption is enabled for the connection.
pub encryption: AtomicBool,
/// Indicates if the client connection is closed.
pub closed: AtomicBool,
/// A unique token identifying the client.
pub token: Token,
pub connection: TcpStream,
pub address: SocketAddr,
enc: PacketEncoder,
dec: PacketDecoder,
pub client_packets_queue: Vec<RawPacket>,
/// The underlying TCP connection to the client.
pub connection: Arc<Mutex<TcpStream>>,
/// The client's IP address.
pub address: Mutex<SocketAddr>,
/// The packet encoder for outgoing packets.
enc: Arc<Mutex<PacketEncoder>>,
/// The packet decoder for incoming packets.
dec: Arc<Mutex<PacketDecoder>>,
/// A queue of raw packets received from the client, waiting to be processed.
pub client_packets_queue: Arc<Mutex<Vec<RawPacket>>>,
pub make_player: bool,
/// Indicates whether the client should be converted into a player.
pub make_player: AtomicBool,
/// Sends each keep alive packet that the server receives for a player to here, which gets picked up in a tokio task
pub keep_alive_sender: Arc<tokio::sync::mpsc::Sender<i64>>,
/// Stores the last time it was confirmed that the client is alive
pub last_alive_received: AtomicCell<std::time::Instant>,
}
impl Client {
pub fn new(token: Token, connection: TcpStream, address: SocketAddr) -> Self {
pub fn new(
token: Token,
connection: TcpStream,
address: SocketAddr,
keep_alive_sender: Arc<tokio::sync::mpsc::Sender<i64>>,
) -> Self {
Self {
protocol_version: 0,
gameprofile: None,
config: None,
brand: None,
protocol_version: AtomicI32::new(0),
gameprofile: Mutex::new(None),
config: Mutex::new(None),
brand: Mutex::new(None),
token,
address,
connection_state: ConnectionState::HandShake,
connection,
enc: PacketEncoder::default(),
dec: PacketDecoder::default(),
encryption: true,
closed: false,
client_packets_queue: Vec::new(),
make_player: false,
address: Mutex::new(address),
connection_state: AtomicCell::new(ConnectionState::HandShake),
connection: Arc::new(Mutex::new(connection)),
enc: Arc::new(Mutex::new(PacketEncoder::default())),
dec: Arc::new(Mutex::new(PacketDecoder::default())),
encryption: AtomicBool::new(false),
closed: AtomicBool::new(false),
client_packets_queue: Arc::new(Mutex::new(Vec::new())),
make_player: AtomicBool::new(false),
keep_alive_sender,
last_alive_received: AtomicCell::new(std::time::Instant::now()),
}
}
/// adds a Incoming packet to the queue
pub fn add_packet(&mut self, packet: RawPacket) {
self.client_packets_queue.push(packet);
/// Adds a Incoming packet to the queue
pub fn add_packet(&self, packet: RawPacket) {
let mut client_packets_queue = self.client_packets_queue.lock();
client_packets_queue.push(packet);
}
/// enables encryption
pub fn enable_encryption(
&mut self,
shared_secret: &[u8], // decrypted
/// Sets the Packet encryption
pub fn set_encryption(
&self,
shared_secret: Option<&[u8]>, // decrypted
) -> Result<(), EncryptionError> {
self.encryption = true;
let crypt_key: [u8; 16] = shared_secret
.try_into()
.map_err(|_| EncryptionError::SharedWrongLength)?;
self.dec.enable_encryption(&crypt_key);
self.enc.enable_encryption(&crypt_key);
if let Some(shared_secret) = shared_secret {
self.encryption
.store(true, std::sync::atomic::Ordering::Relaxed);
let crypt_key: [u8; 16] = shared_secret
.try_into()
.map_err(|_| EncryptionError::SharedWrongLength)?;
self.dec.lock().set_encryption(Some(&crypt_key));
self.enc.lock().set_encryption(Some(&crypt_key));
} else {
self.dec.lock().set_encryption(None);
self.enc.lock().set_encryption(None);
}
Ok(())
}
// Compression threshold, Compression level
pub fn set_compression(&mut self, compression: Option<(u32, u32)>) {
self.dec.set_compression(compression.map(|v| v.0));
self.enc.set_compression(compression);
/// Sets the Packet compression
pub fn set_compression(&self, compression: Option<CompressionInfo>) {
self.dec.lock().set_compression(compression.is_some());
self.enc.lock().set_compression(compression);
}
/// Send a Clientbound Packet to the Client
pub fn send_packet<P: ClientPacket>(&mut self, packet: &P) {
pub fn send_packet<P: ClientPacket>(&self, packet: &P) {
// assert!(!self.closed);
self.enc
.append_packet(packet)
let mut enc = self.enc.lock();
enc.append_packet(packet)
.unwrap_or_else(|e| self.kick(&e.to_string()));
self.connection
.write_all(&self.enc.take())
.lock()
.write_all(&enc.take())
.map_err(|_| PacketError::ConnectionWrite)
.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>(&self, packet: &P) -> Result<(), PacketError> {
// assert!(!self.closed);
self.enc.append_packet(packet)?;
let mut enc = self.enc.lock();
enc.append_packet(packet)?;
self.connection
.write_all(&self.enc.take())
.lock()
.write_all(&enc.take())
.map_err(|_| PacketError::ConnectionWrite)?;
Ok(())
}
pub async fn process_packets(&mut self, server: &Arc<Server>) {
while let Some(mut packet) = self.client_packets_queue.pop() {
match self.handle_packet(server, &mut packet).await {
Ok(_) => {}
Err(e) => {
let text = format!("Error while reading incoming packet {}", e);
log::error!("{}", text);
self.kick(&text)
}
};
/// Processes all packets send by the client
pub async fn process_packets(&self, server: &Arc<Server>) {
while let Some(mut packet) = self.client_packets_queue.lock().pop() {
let _ = self.handle_packet(server, &mut packet).await.map_err(|e| {
let text = format!("Error while reading incoming packet {}", e);
log::error!("{}", text);
self.kick(&text)
});
}
}
/// Handles an incoming decoded not Play state Packet
pub async fn handle_packet(
&mut self,
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
// TODO: handle each packet's Error instead of calling .unwrap()
let bytebuf = &mut packet.bytebuf;
match self.connection_state {
pumpkin_protocol::ConnectionState::HandShake => match packet.id.0 {
SHandShake::PACKET_ID => {
self.handle_handshake(server, SHandShake::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Handshake state",
packet.id.0
);
Ok(())
}
},
pumpkin_protocol::ConnectionState::Status => match packet.id.0 {
SStatusRequest::PACKET_ID => {
self.handle_status_request(server, SStatusRequest::read(bytebuf)?);
Ok(())
}
SStatusPingRequest::PACKET_ID => {
self.handle_ping_request(server, SStatusPingRequest::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Status state",
packet.id.0
);
Ok(())
}
},
match self.connection_state.load() {
pumpkin_protocol::ConnectionState::HandShake => {
self.handle_handshake_packet(server, packet)
}
pumpkin_protocol::ConnectionState::Status => self.handle_status_packet(server, packet),
// TODO: Check config if transfer is enabled
pumpkin_protocol::ConnectionState::Login
| pumpkin_protocol::ConnectionState::Transfer => match packet.id.0 {
SLoginStart::PACKET_ID => {
self.handle_login_start(server, SLoginStart::read(bytebuf)?);
Ok(())
}
SEncryptionResponse::PACKET_ID => {
self.handle_encryption_response(server, SEncryptionResponse::read(bytebuf)?)
.await;
Ok(())
}
SLoginPluginResponse::PACKET_ID => {
self.handle_plugin_response(server, SLoginPluginResponse::read(bytebuf)?);
Ok(())
}
SLoginAcknowledged::PACKET_ID => {
self.handle_login_acknowledged(server, SLoginAcknowledged::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Login state",
packet.id.0
);
Ok(())
}
},
pumpkin_protocol::ConnectionState::Config => match packet.id.0 {
SClientInformationConfig::PACKET_ID => {
self.handle_client_information_config(
server,
SClientInformationConfig::read(bytebuf)?,
);
Ok(())
}
SPluginMessage::PACKET_ID => {
self.handle_plugin_message(server, SPluginMessage::read(bytebuf)?);
Ok(())
}
SAcknowledgeFinishConfig::PACKET_ID => {
self.handle_config_acknowledged(
server,
SAcknowledgeFinishConfig::read(bytebuf)?,
)
.await;
Ok(())
}
SKnownPacks::PACKET_ID => {
self.handle_known_packs(server, SKnownPacks::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Config state",
packet.id.0
);
Ok(())
}
},
| pumpkin_protocol::ConnectionState::Transfer => {
self.handle_login_packet(server, packet).await
}
pumpkin_protocol::ConnectionState::Config => {
self.handle_config_packet(server, packet).await
}
_ => {
log::error!("Invalid Connection state {:?}", self.connection_state);
Ok(())
@@ -268,15 +241,133 @@ impl Client {
}
}
// Reads the connection until our buffer of len 4096 is full, then decode
/// Close connection when an error occurs
pub async fn poll(&mut self, event: &Event) {
fn handle_handshake_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SHandShake::PACKET_ID => {
self.handle_handshake(server, SHandShake::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Handshake state",
packet.id.0
);
Ok(())
}
}
}
fn handle_status_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SStatusRequest::PACKET_ID => {
self.handle_status_request(server, SStatusRequest::read(bytebuf)?);
Ok(())
}
SStatusPingRequest::PACKET_ID => {
self.handle_ping_request(server, SStatusPingRequest::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Status state",
packet.id.0
);
Ok(())
}
}
}
async fn handle_login_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SLoginStart::PACKET_ID => {
self.handle_login_start(server, SLoginStart::read(bytebuf)?);
Ok(())
}
SEncryptionResponse::PACKET_ID => {
self.handle_encryption_response(server, SEncryptionResponse::read(bytebuf)?)
.await;
Ok(())
}
SLoginPluginResponse::PACKET_ID => {
self.handle_plugin_response(server, SLoginPluginResponse::read(bytebuf)?);
Ok(())
}
SLoginAcknowledged::PACKET_ID => {
self.handle_login_acknowledged(server, SLoginAcknowledged::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Login state",
packet.id.0
);
Ok(())
}
}
}
async fn handle_config_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), DeserializerError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SClientInformationConfig::PACKET_ID => {
self.handle_client_information_config(
server,
SClientInformationConfig::read(bytebuf)?,
);
Ok(())
}
SPluginMessage::PACKET_ID => {
self.handle_plugin_message(server, SPluginMessage::read(bytebuf)?);
Ok(())
}
SAcknowledgeFinishConfig::PACKET_ID => {
self.handle_config_acknowledged(server, SAcknowledgeFinishConfig::read(bytebuf)?)
.await;
Ok(())
}
SKnownPacks::PACKET_ID => {
self.handle_known_packs(server, SKnownPacks::read(bytebuf)?);
Ok(())
}
_ => {
log::error!(
"Failed to handle packet id {} while in Config state",
packet.id.0
);
Ok(())
}
}
}
/// Reads the connection until our buffer of len 4096 is full, then decode
/// Close connection when an error occurs or when the Client closed the connection
pub async fn poll(&self, event: &Event) {
if event.is_readable() {
let mut received_data = vec![0; 4096];
let mut bytes_read = 0;
// We can (maybe) read from the connection.
loop {
match self.connection.read(&mut received_data[bytes_read..]) {
let connection = self.connection.clone();
let mut connection = connection.lock();
match connection.read(&mut received_data[bytes_read..]) {
Ok(0) => {
// Reading 0 bytes means the other side has closed the
// connection or is done writing, then so are we.
@@ -297,9 +388,9 @@ impl Client {
}
if bytes_read != 0 {
self.dec.reserve(4096);
self.dec.queue_slice(&received_data[..bytes_read]);
match self.dec.decode() {
let mut dec = self.dec.lock();
dec.queue_slice(&received_data[..bytes_read]);
match dec.decode() {
Ok(packet) => {
if let Some(packet) = packet {
self.add_packet(packet);
@@ -307,18 +398,18 @@ impl Client {
}
Err(err) => self.kick(&err.to_string()),
}
self.dec.clear();
dec.clear();
}
}
}
/// Kicks the Client with a reason depending on the connection state
pub fn kick(&mut self, reason: &str) {
pub fn kick(&self, reason: &str) {
dbg!(reason);
match self.connection_state {
match self.connection_state.load() {
ConnectionState::Login => {
self.try_send_packet(&CLoginDisconnect::new(
&serde_json::to_string_pretty(&reason).unwrap_or("".into()),
&serde_json::to_string_pretty(&reason).unwrap_or_else(|_| "".into()),
))
.unwrap_or_else(|_| self.close());
}
@@ -326,7 +417,7 @@ impl Client {
self.try_send_packet(&CConfigDisconnect::new(reason))
.unwrap_or_else(|_| self.close());
}
// So we can also kick on errors, but generally should use Player::kick
// This way players get kicked when players using client functions (e.g. poll, send_packet)
ConnectionState::Play => {
self.try_send_packet(&CPlayDisconnect::new(&TextComponent::text(reason)))
.unwrap_or_else(|_| self.close());
@@ -339,8 +430,9 @@ impl Client {
}
/// You should prefer to use `kick` when you can
pub fn close(&mut self) {
self.closed = true;
pub fn close(&self) {
self.closed
.store(true, std::sync::atomic::Ordering::Relaxed);
}
}

View File

@@ -9,7 +9,7 @@ use crate::{
use num_traits::FromPrimitive;
use pumpkin_config::ADVANCED_CONFIG;
use pumpkin_core::{
math::{position::WorldPosition, wrap_degrees},
math::{position::WorldPosition, vector3::Vector3, wrap_degrees},
text::TextComponent,
GameMode,
};
@@ -29,7 +29,7 @@ use pumpkin_protocol::{
SUseItemOn, Status,
},
};
use pumpkin_world::block::{BlockFace, BlockId};
use pumpkin_world::block::{BlockFace, BlockState};
use pumpkin_world::global_registry;
use super::PlayerConfig;
@@ -42,16 +42,19 @@ fn modulus(a: f32, b: f32) -> f32 {
/// NEVER TRUST THE CLIENT. HANDLE EVERY ERROR, UNWRAP/EXPECT ARE FORBIDDEN
impl Player {
pub fn handle_confirm_teleport(
&mut self,
&self,
_server: &Arc<Server>,
confirm_teleport: SConfirmTeleport,
) {
if let Some((id, position)) = self.awaiting_teleport.as_ref() {
let mut awaiting_teleport = self.awaiting_teleport.lock();
if let Some((id, position)) = awaiting_teleport.as_ref() {
if id == &confirm_teleport.teleport_id {
// we should set the pos now to that we requested in the teleport packet, Is may fixed issues when the client sended position packets while being teleported
self.entity.set_pos(position.x, position.y, position.z);
self.living_entity
.entity
.set_pos(position.x, position.y, position.z);
self.awaiting_teleport = None;
*awaiting_teleport = None;
} else {
self.kick(TextComponent::text("Wrong teleport id"))
}
@@ -70,25 +73,27 @@ impl Player {
pos.clamp(-2.0E7, 2.0E7)
}
pub async fn handle_position(&mut self, _server: &Arc<Server>, position: SPlayerPosition) {
pub async fn handle_position(&self, _server: &Arc<Server>, position: SPlayerPosition) {
if position.x.is_nan() || position.feet_y.is_nan() || position.z.is_nan() {
self.kick(TextComponent::text("Invalid movement"));
return;
}
let entity = &mut self.entity;
self.last_position = entity.pos;
let entity = &self.living_entity.entity;
entity.set_pos(
Self::clamp_horizontal(position.x),
Self::clamp_vertical(position.feet_y),
Self::clamp_horizontal(position.z),
);
entity.on_ground = position.ground;
let on_ground = entity.on_ground;
let pos = entity.pos.load();
self.last_position.store(pos);
let last_position = self.last_position.load();
entity
.on_ground
.store(position.ground, std::sync::atomic::Ordering::Relaxed);
let entity_id = entity.entity_id;
let (x, y, z) = entity.pos.into();
let (lastx, lasty, lastz) = self.last_position.into();
let world = self.entity.world.clone();
let world = world.lock().await;
let Vector3 { x, y, z } = pos;
let (lastx, lasty, lastz) = (last_position.x, last_position.y, last_position.z);
let world = &entity.world;
// let delta = Vector3::new(x - lastx, y - lasty, z - lastz);
// let velocity = self.velocity;
@@ -103,21 +108,21 @@ impl Player {
// return;
// }
// send new position to all other players
world.broadcast_packet(
world.broadcast_packet_expect(
&[self.client.token],
&CUpdateEntityPos::new(
entity_id.into(),
(x * 4096.0 - lastx * 4096.0) as i16,
(y * 4096.0 - lasty * 4096.0) as i16,
(z * 4096.0 - lastz * 4096.0) as i16,
on_ground,
x.mul_add(4096.0, -(lastx * 4096.0)) as i16,
y.mul_add(4096.0, -(lasty * 4096.0)) as i16,
z.mul_add(4096.0, -(lastz * 4096.0)) as i16,
position.ground,
),
);
player_chunker::update_position(&world, self).await;
player_chunker::update_position(entity, self).await;
}
pub async fn handle_position_rotation(
&mut self,
&self,
_server: &Arc<Server>,
position_rotation: SPlayerPositionRotation,
) {
@@ -128,31 +133,36 @@ impl Player {
self.kick(TextComponent::text("Invalid movement"));
return;
}
if !position_rotation.yaw.is_finite() || !position_rotation.pitch.is_finite() {
if position_rotation.yaw.is_infinite() || position_rotation.pitch.is_infinite() {
self.kick(TextComponent::text("Invalid rotation"));
return;
}
let entity = &mut self.entity;
let entity = &self.living_entity.entity;
self.last_position = entity.pos;
entity.set_pos(
Self::clamp_horizontal(position_rotation.x),
Self::clamp_vertical(position_rotation.feet_y),
Self::clamp_horizontal(position_rotation.z),
);
entity.on_ground = position_rotation.ground;
entity.yaw = wrap_degrees(position_rotation.yaw) % 360.0;
entity.pitch = wrap_degrees(position_rotation.pitch).clamp(-90.0, 90.0) % 360.0;
let pos = entity.pos.load();
self.last_position.store(pos);
let last_position = self.last_position.load();
entity.on_ground.store(
position_rotation.ground,
std::sync::atomic::Ordering::Relaxed,
);
entity.set_rotation(
wrap_degrees(position_rotation.yaw) % 360.0,
wrap_degrees(position_rotation.pitch).clamp(-90.0, 90.0) % 360.0,
);
let on_ground = entity.on_ground;
let entity_id = entity.entity_id;
let (x, y, z) = entity.pos.into();
let (lastx, lasty, lastz) = self.last_position.into();
let yaw = modulus(entity.yaw * 256.0 / 360.0, 256.0);
let pitch = modulus(entity.pitch * 256.0 / 360.0, 256.0);
let Vector3 { x, y, z } = pos;
let (lastx, lasty, lastz) = (last_position.x, last_position.y, last_position.z);
let yaw = modulus(entity.yaw.load() * 256.0 / 360.0, 256.0);
let pitch = modulus(entity.pitch.load() * 256.0 / 360.0, 256.0);
// let head_yaw = (entity.head_yaw * 256.0 / 360.0).floor();
let world = self.entity.world.clone();
let world = world.lock().await;
let world = &entity.world;
// let delta = Vector3::new(x - lastx, y - lasty, z - lastz);
// let velocity = self.velocity;
@@ -168,97 +178,111 @@ impl Player {
// }
// send new position to all other players
world.broadcast_packet(
world.broadcast_packet_expect(
&[self.client.token],
&CUpdateEntityPosRot::new(
entity_id.into(),
(x * 4096.0 - lastx * 4096.0) as i16,
(y * 4096.0 - lasty * 4096.0) as i16,
(z * 4096.0 - lastz * 4096.0) as i16,
x.mul_add(4096.0, -(lastx * 4096.0)) as i16,
y.mul_add(4096.0, -(lasty * 4096.0)) as i16,
z.mul_add(4096.0, -(lastz * 4096.0)) as i16,
yaw as u8,
pitch as u8,
on_ground,
position_rotation.ground,
),
);
world.broadcast_packet(
world.broadcast_packet_expect(
&[self.client.token],
&CHeadRot::new(entity_id.into(), yaw as u8),
);
player_chunker::update_position(&world, self).await;
player_chunker::update_position(entity, self).await;
}
pub async fn handle_rotation(&mut self, _server: &Arc<Server>, rotation: SPlayerRotation) {
pub async fn handle_rotation(&self, _server: &Arc<Server>, rotation: SPlayerRotation) {
if !rotation.yaw.is_finite() || !rotation.pitch.is_finite() {
self.kick(TextComponent::text("Invalid rotation"));
return;
}
let entity = &mut self.entity;
entity.on_ground = rotation.ground;
entity.yaw = wrap_degrees(rotation.yaw) % 360.0;
entity.pitch = wrap_degrees(rotation.pitch).clamp(-90.0, 90.0) % 360.0;
let entity = &self.living_entity.entity;
entity
.on_ground
.store(rotation.ground, std::sync::atomic::Ordering::Relaxed);
entity.set_rotation(
wrap_degrees(rotation.yaw) % 360.0,
wrap_degrees(rotation.pitch).clamp(-90.0, 90.0) % 360.0,
);
// send new position to all other players
let on_ground = entity.on_ground;
let entity_id = entity.entity_id;
let yaw = modulus(entity.yaw * 256.0 / 360.0, 256.0);
let pitch = modulus(entity.pitch * 256.0 / 360.0, 256.0);
let yaw = modulus(entity.yaw.load() * 256.0 / 360.0, 256.0);
let pitch = modulus(entity.pitch.load() * 256.0 / 360.0, 256.0);
// let head_yaw = modulus(entity.head_yaw * 256.0 / 360.0, 256.0);
let world = self.entity.world.lock().await;
let packet = CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, on_ground);
// self.client.send_packet(&packet);
world.broadcast_packet(&[self.client.token], &packet);
let world = &entity.world;
let packet =
CUpdateEntityRot::new(entity_id.into(), yaw as u8, pitch as u8, rotation.ground);
world.broadcast_packet_expect(&[self.client.token], &packet);
let packet = CHeadRot::new(entity_id.into(), yaw as u8);
// self.client.send_packet(&packet);
world.broadcast_packet(&[self.client.token], &packet);
world.broadcast_packet_expect(&[self.client.token], &packet);
}
pub fn handle_chat_command(&mut self, server: &Arc<Server>, command: SChatCommand) {
pub fn handle_chat_command(&self, server: &Arc<Server>, command: SChatCommand) {
let dispatcher = server.command_dispatcher.clone();
dispatcher.handle_command(&mut CommandSender::Player(self), server, &command.command);
if ADVANCED_CONFIG.commands.log_console {
log::info!(
"Player ({}): executed command /{}",
self.gameprofile.name,
command.command
);
}
}
pub fn handle_player_ground(&mut self, _server: &Arc<Server>, ground: SSetPlayerGround) {
self.entity.on_ground = ground.on_ground;
pub fn handle_player_ground(&self, _server: &Arc<Server>, ground: SSetPlayerGround) {
self.living_entity
.entity
.on_ground
.store(ground.on_ground, std::sync::atomic::Ordering::Relaxed);
}
pub async fn handle_player_command(&mut self, _server: &Arc<Server>, command: SPlayerCommand) {
if command.entity_id != self.entity.entity_id.into() {
pub async fn handle_player_command(&self, _server: &Arc<Server>, command: SPlayerCommand) {
if command.entity_id != self.entity_id().into() {
return;
}
if let Some(action) = Action::from_i32(command.action.0) {
let entity = &self.living_entity.entity;
match action {
pumpkin_protocol::server::play::Action::StartSneaking => {
if !self.entity.sneaking {
self.entity.set_sneaking(&mut self.client, true).await
if !entity.sneaking.load(std::sync::atomic::Ordering::Relaxed) {
entity.set_sneaking(true).await
}
}
pumpkin_protocol::server::play::Action::StopSneaking => {
if self.entity.sneaking {
self.entity.set_sneaking(&mut self.client, false).await
if entity.sneaking.load(std::sync::atomic::Ordering::Relaxed) {
entity.set_sneaking(false).await
}
}
pumpkin_protocol::server::play::Action::LeaveBed => todo!(),
pumpkin_protocol::server::play::Action::StartSprinting => {
if !self.entity.sprinting {
self.entity.set_sprinting(&mut self.client, true).await
if !entity.sprinting.load(std::sync::atomic::Ordering::Relaxed) {
entity.set_sprinting(true).await
}
}
pumpkin_protocol::server::play::Action::StopSprinting => {
if self.entity.sprinting {
self.entity.set_sprinting(&mut self.client, false).await
if entity.sprinting.load(std::sync::atomic::Ordering::Relaxed) {
entity.set_sprinting(false).await
}
}
pumpkin_protocol::server::play::Action::StartHorseJump => todo!(),
pumpkin_protocol::server::play::Action::StopHorseJump => todo!(),
pumpkin_protocol::server::play::Action::OpenVehicleInventory => todo!(),
pumpkin_protocol::server::play::Action::StartFlyingElytra => {
let fall_flying = self.entity.check_fall_flying();
if self.entity.fall_flying != fall_flying {
self.entity
.set_fall_flying(&mut self.client, fall_flying)
.await;
let fall_flying = entity.check_fall_flying();
if entity
.fall_flying
.load(std::sync::atomic::Ordering::Relaxed)
!= fall_flying
{
entity.set_fall_flying(fall_flying).await;
}
} // TODO
}
@@ -267,7 +291,7 @@ impl Player {
}
}
pub async fn handle_swing_arm(&mut self, _server: &Arc<Server>, swing_arm: SSwingArm) {
pub async fn handle_swing_arm(&self, _server: &Arc<Server>, swing_arm: SSwingArm) {
match Hand::from_i32(swing_arm.hand.0) {
Some(hand) => {
let animation = match hand {
@@ -275,8 +299,8 @@ impl Player {
Hand::Off => Animation::SwingOffhand,
};
let id = self.entity_id();
let world = self.entity.world.lock().await;
world.broadcast_packet(
let world = &self.living_entity.entity.world;
world.broadcast_packet_expect(
&[self.client.token],
&CEntityAnimation::new(id.into(), animation as u8),
)
@@ -287,7 +311,7 @@ impl Player {
};
}
pub async fn handle_chat_message(&mut self, _server: &Arc<Server>, chat_message: SChatMessage) {
pub async fn handle_chat_message(&self, _server: &Arc<Server>, chat_message: SChatMessage) {
dbg!("got message");
let message = chat_message.message;
@@ -299,24 +323,22 @@ impl Player {
// TODO: filter message & validation
let gameprofile = &self.gameprofile;
let world = self.entity.world.lock().await;
world.broadcast_packet(
&[self.client.token],
&CPlayerChatMessage::new(
pumpkin_protocol::uuid::UUID(gameprofile.id),
1.into(),
chat_message.signature.as_deref(),
&message,
chat_message.timestamp,
chat_message.salt,
&[],
Some(TextComponent::text(&message)),
FilterType::PassThrough,
1.into(),
TextComponent::text(&gameprofile.name.clone()),
None,
),
)
let entity = &self.living_entity.entity;
let world = &entity.world;
world.broadcast_packet_all(&CPlayerChatMessage::new(
gameprofile.id,
1.into(),
chat_message.signature.as_deref(),
&message,
chat_message.timestamp,
chat_message.salt,
&[],
Some(TextComponent::text(&message)),
FilterType::PassThrough,
1.into(),
TextComponent::text(&gameprofile.name),
None,
))
/* server.broadcast_packet(
self,
@@ -330,7 +352,7 @@ impl Player {
}
pub fn handle_client_information_play(
&mut self,
&self,
_server: &Arc<Server>,
client_information: SClientInformationPlay,
) {
@@ -338,7 +360,7 @@ impl Player {
Hand::from_i32(client_information.main_hand.into()),
ChatMode::from_i32(client_information.chat_mode.into()),
) {
self.config = PlayerConfig {
*self.config.lock() = PlayerConfig {
locale: client_information.locale,
view_distance: client_information.view_distance,
chat_mode,
@@ -353,10 +375,11 @@ impl Player {
}
}
pub async fn handle_interact(&mut self, _: &Arc<Server>, interact: SInteract) {
pub async fn handle_interact(&self, _: &Arc<Server>, interact: SInteract) {
let sneaking = interact.sneaking;
if self.entity.sneaking != sneaking {
self.entity.set_sneaking(&mut self.client, sneaking).await;
let entity = &self.living_entity.entity;
if entity.sneaking.load(std::sync::atomic::Ordering::Relaxed) != sneaking {
entity.set_sneaking(sneaking).await;
}
match ActionType::from_i32(interact.typ.0) {
Some(action) => match action {
@@ -365,44 +388,44 @@ impl Player {
// TODO: do validation and stuff
let config = &ADVANCED_CONFIG.pvp;
if config.enabled {
let world = self.entity.world.clone();
let world = world.lock().await;
let attacked_player = world.get_by_entityid(self, entity_id.0 as EntityId);
if let Some(mut player) = attacked_player {
let token = player.client.token;
let velo = player.entity.velocity;
if config.protect_creative && player.gamemode == GameMode::Creative {
let world = &entity.world;
let attacked_player = world.get_player_by_entityid(entity_id.0 as EntityId);
if let Some(player) = attacked_player {
let victem_entity = &player.living_entity.entity;
if config.protect_creative
&& player.gamemode.load() == GameMode::Creative
{
return;
}
if config.knockback {
let yaw = self.entity.yaw;
let yaw = entity.yaw.load();
let strength = 1.0;
player.entity.knockback(
let victem_velocity = victem_entity.velocity.load();
let saved_velo = victem_velocity;
victem_entity.knockback(
strength * 0.5,
(yaw * (PI / 180.0)).sin() as f64,
-(yaw * (PI / 180.0)).cos() as f64,
);
let packet = &CEntityVelocity::new(
&entity_id,
velo.x as f32,
velo.y as f32,
velo.z as f32,
victem_velocity.x as f32,
victem_velocity.y as f32,
victem_velocity.z as f32,
);
self.entity.velocity = self.entity.velocity.multiply(0.6, 1.0, 0.6);
let velocity = entity.velocity.load();
victem_entity
.velocity
.store(velocity.multiply(0.6, 1.0, 0.6));
player.entity.velocity = velo;
victem_entity.velocity.store(saved_velo);
player.client.send_packet(packet);
}
if config.hurt_animation {
// TODO
// thats how we prevent borrow errors :c
let packet = &CHurtAnimation::new(&entity_id, self.entity.yaw);
self.client.send_packet(packet);
player.client.send_packet(packet);
world.broadcast_packet(
&[self.client.token, token],
&CHurtAnimation::new(&entity_id, 10.0),
)
world.broadcast_packet_all(&CHurtAnimation::new(
&entity_id,
entity.yaw.load(),
))
}
if config.swing {}
} else {
@@ -420,11 +443,7 @@ impl Player {
None => self.kick(TextComponent::text("Invalid action type")),
}
}
pub async fn handle_player_action(
&mut self,
_server: &Arc<Server>,
player_action: SPlayerAction,
) {
pub async fn handle_player_action(&self, _server: &Arc<Server>, player_action: SPlayerAction) {
match Status::from_i32(player_action.status.0) {
Some(status) => match status {
Status::StartedDigging => {
@@ -434,20 +453,15 @@ impl Player {
}
// TODO: do validation
// TODO: Config
if self.gamemode == GameMode::Creative {
if self.gamemode.load() == GameMode::Creative {
let location = player_action.location;
// Block break & block break sound
// TODO: currently this is always dirt replace it
let world = self.entity.world.lock().await;
world.broadcast_packet(
&[self.client.token],
&CWorldEvent::new(2001, &location, 11, false),
);
let entity = &self.living_entity.entity;
let world = &entity.world;
world.broadcast_packet_all(&CWorldEvent::new(2001, &location, 11, false));
// AIR
world.broadcast_packet(
&[self.client.token],
&CBlockUpdate::new(&location, 0.into()),
);
world.broadcast_packet_all(&CBlockUpdate::new(&location, 0.into()));
}
}
Status::CancelledDigging => {
@@ -455,7 +469,8 @@ impl Player {
// TODO: maybe log?
return;
}
self.current_block_destroy_stage = 0;
self.current_block_destroy_stage
.store(0, std::sync::atomic::Ordering::Relaxed);
}
Status::FinishedDigging => {
// TODO: do validation
@@ -466,16 +481,11 @@ impl Player {
}
// Block break & block break sound
// TODO: currently this is always dirt replace it
let world = self.entity.world.lock().await;
world.broadcast_packet(
&[self.client.token],
&CWorldEvent::new(2001, &location, 11, false),
);
let entity = &self.living_entity.entity;
let world = &entity.world;
world.broadcast_packet_all(&CWorldEvent::new(2001, &location, 11, false));
// AIR
world.broadcast_packet(
&[self.client.token],
&CBlockUpdate::new(&location, 0.into()),
);
world.broadcast_packet_all(&CBlockUpdate::new(&location, 0.into()));
// TODO: Send this every tick
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence));
@@ -497,12 +507,12 @@ impl Player {
}
}
pub fn handle_play_ping_request(&mut self, _server: &Arc<Server>, request: SPlayPingRequest) {
pub fn handle_play_ping_request(&self, _server: &Arc<Server>, request: SPlayPingRequest) {
self.client
.send_packet(&CPingResponse::new(request.payload));
}
pub async fn handle_use_item_on(&mut self, _server: &Arc<Server>, use_item_on: SUseItemOn) {
pub async fn handle_use_item_on(&self, _server: &Arc<Server>, use_item_on: SUseItemOn) {
let location = use_item_on.location;
if !self.can_interact_with_block_at(&location, 1.0) {
@@ -511,25 +521,23 @@ impl Player {
}
if let Some(face) = BlockFace::from_i32(use_item_on.face.0) {
if let Some(item) = self.inventory.held_item() {
if let Some(item) = self.inventory.lock().held_item() {
let minecraft_id = global_registry::find_minecraft_id(
global_registry::ITEM_REGISTRY,
item.item_id,
)
.expect("All item ids are in the global registry");
if let Ok(block_state_id) = BlockId::new(minecraft_id, None) {
let world = self.entity.world.lock().await;
world.broadcast_packet(
&[self.client.token],
&CBlockUpdate::new(&location, block_state_id.get_id_mojang_repr().into()),
);
world.broadcast_packet(
&[self.client.token],
&CBlockUpdate::new(
&WorldPosition(location.0 + face.to_offset()),
block_state_id.get_id_mojang_repr().into(),
),
);
if let Ok(block_state_id) = BlockState::new(minecraft_id, None) {
let entity = &self.living_entity.entity;
let world = &entity.world;
world.broadcast_packet_all(&CBlockUpdate::new(
&location,
block_state_id.get_id_mojang_repr().into(),
));
world.broadcast_packet_all(&CBlockUpdate::new(
&WorldPosition(location.0 + face.to_offset()),
block_state_id.get_id_mojang_repr().into(),
));
}
}
self.client
@@ -539,46 +547,48 @@ impl Player {
}
}
pub fn handle_use_item(&mut self, _server: &Arc<Server>, _use_item: SUseItem) {
pub fn handle_use_item(&self, _server: &Arc<Server>, _use_item: SUseItem) {
// TODO: handle packet correctly
log::error!("An item was used(SUseItem), but the packet is not implemented yet");
}
pub fn handle_set_held_item(&mut self, _server: &Arc<Server>, held: SSetHeldItem) {
pub fn handle_set_held_item(&self, _server: &Arc<Server>, held: SSetHeldItem) {
let slot = held.slot;
if !(0..=8).contains(&slot) {
self.kick(TextComponent::text("Invalid held slot"))
}
self.inventory.set_selected(slot as usize);
self.inventory.lock().set_selected(slot as usize);
}
pub fn handle_set_creative_slot(
&mut self,
&self,
_server: &Arc<Server>,
packet: SSetCreativeSlot,
) -> Result<(), InventoryError> {
if self.gamemode != GameMode::Creative {
if self.gamemode.load() != GameMode::Creative {
return Err(InventoryError::PermissionError);
}
self.inventory
.lock()
.set_slot(packet.slot as usize, packet.clicked_item.to_item(), false)
}
// TODO:
// This function will in the future be used to keep track of if the client is in a valid state.
// But this is not possible yet
pub fn handle_close_container(&mut self, server: &Arc<Server>, packet: SCloseContainer) {
pub fn handle_close_container(&self, server: &Arc<Server>, packet: SCloseContainer) {
// window_id 0 represents both 9x1 Generic AND inventory here
self.inventory.state_id = 0;
if let Some(id) = self.open_container {
let mut open_containers = server
.open_containers
.write()
.expect("open_containers got poisoned");
self.inventory
.lock()
.state_id
.store(0, std::sync::atomic::Ordering::Relaxed);
let open_container = self.open_container.load();
if let Some(id) = open_container {
let mut open_containers = server.open_containers.write();
if let Some(container) = open_containers.get_mut(&id) {
container.remove_player(self.entity_id())
}
self.open_container = None;
self.open_container.store(None);
}
let Some(_window_type) = WindowType::from_u8(packet.window_id) else {
self.kick(TextComponent::text("Invalid window ID"));

View File

@@ -1,8 +1,11 @@
use std::sync::Arc;
use crate::commands::dispatcher::InvalidTreeError;
use crate::commands::dispatcher::InvalidTreeError::InvalidConsumptionError;
use crate::commands::tree::{ConsumedArgs, RawArgs};
use crate::commands::CommandSender;
use crate::commands::CommandSender::Player;
use crate::server::Server;
/// todo: implement (so far only own name + @s/@p is implemented)
pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option<String> {
@@ -29,9 +32,10 @@ pub fn consume_arg_player(src: &CommandSender, args: &mut RawArgs) -> Option<Str
/// todo: implement (so far only own name + @s/@p is implemented)
pub fn parse_arg_player<'a>(
src: &'a mut CommandSender,
_server: &Arc<Server>,
arg_name: &str,
consumed_args: &ConsumedArgs,
) -> Result<&'a mut crate::entity::player::Player, InvalidTreeError> {
) -> Result<&'a crate::entity::player::Player, InvalidTreeError> {
let s = consumed_args
.get(arg_name)
.ok_or(InvalidConsumptionError(None))?

View File

@@ -7,16 +7,13 @@ const NAMES: [&str; 2] = ["echest", "enderchest"];
const DESCRIPTION: &str =
"Show your personal enderchest (this command is used for testing container behaviour)";
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
pub fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(NAMES, DESCRIPTION).execute(&|sender, server, _| {
if let Some(player) = sender.as_mut_player() {
let entity_id = player.entity_id();
player.open_container = Some(0);
player.open_container.store(Some(0));
{
let mut open_containers = server
.open_containers
.write()
.expect("open_containers got poisoned");
let mut open_containers = server.open_containers.write();
match open_containers.get_mut(&0) {
Some(ender_chest) => {
ender_chest.add_player(entity_id);

View File

@@ -56,7 +56,7 @@ pub fn parse_arg_gamemode(consumed_args: &ConsumedArgs) -> Result<GameMode, Inva
}
}
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
pub fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(NAMES, DESCRIPTION).with_child(
require(&|sender| sender.permission_lvl() >= 2).with_child(
argument(ARG_GAMEMODE, consume_arg_gamemode)
@@ -65,15 +65,14 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
let gamemode = parse_arg_gamemode(args)?;
return if let Player(target) = sender {
if target.gamemode == gamemode {
if target.gamemode.load() == gamemode {
target.send_system_message(TextComponent::text(&format!(
"You already in {:?} gamemode",
gamemode
)));
} else {
// TODO
#[expect(clippy::let_underscore_future)]
let _ = target.set_gamemode(gamemode);
target.set_gamemode(gamemode);
target.send_system_message(TextComponent::text(&format!(
"Game mode was set to {:?}",
gamemode
@@ -86,19 +85,18 @@ pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
}),
)
.with_child(argument(ARG_TARGET, consume_arg_player).execute(
&|sender, _, args| {
&|sender, server, args| {
let gamemode = parse_arg_gamemode(args)?;
let target = parse_arg_player(sender, ARG_TARGET, args)?;
let target = parse_arg_player(sender, server, ARG_TARGET, args)?;
if target.gamemode == gamemode {
if target.gamemode.load() == gamemode {
target.send_system_message(TextComponent::text(&format!(
"You already in {:?} gamemode",
gamemode
)));
} else {
// TODO
#[expect(clippy::let_underscore_future)]
let _ = target.set_gamemode(gamemode);
target.set_gamemode(gamemode);
target.send_system_message(TextComponent::text(&format!(
"Game mode was set to {:?}",
gamemode

View File

@@ -32,7 +32,7 @@ fn parse_arg_command<'a>(
.map_err(|_| InvalidConsumptionError(Some(command_name.into())))
}
pub(crate) fn init_command_tree<'a>() -> CommandTree<'a> {
pub fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(NAMES, DESCRIPTION)
.with_child(
argument(ARG_COMMAND, consume_arg_command).execute(&|sender, server, args| {

Some files were not shown because too many files have changed in this diff Show More