mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
Merge branch 'master' into pumpkinerror-trait
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
# Allow the source code folders
|
||||
!/pumpkin*/
|
||||
!/assets
|
||||
|
||||
# Dependencies
|
||||
!Cargo.lock
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
514
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
52
README.md
52
README.md
@@ -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.
|
||||
|
||||

|
||||
@@ -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.
|
||||
|
||||
17
STRUCTURE.md
17
STRUCTURE.md
@@ -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
|
||||
@@ -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" }]],
|
||||
});
|
||||
|
||||
@@ -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
427
docs/config/advanced.md
Normal 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
117
docs/config/basic.md
Normal 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
|
||||
```
|
||||
16
docs/config/introduction.md
Normal file
16
docs/config/introduction.md
Normal 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.
|
||||
73
docs/developer/authentication.md
Normal file
73
docs/developer/authentication.md
Normal 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,
|
||||
}
|
||||
```
|
||||
8
docs/developer/introduction.md
Normal file
8
docs/developer/introduction.md
Normal 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.
|
||||
272
docs/developer/networking.md
Normal file
272
docs/developer/networking.md
Normal 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`
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
# Getting Started in Rust
|
||||
|
||||
Rust in one of the supported plugin languages.
|
||||
This page has not been written yet.
|
||||
33
docs/troubleshooting/common_issues.md
Normal file
33
docs/troubleshooting/common_issues.md
Normal 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
1765
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,4 @@ serde.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
toml = "0.8"
|
||||
|
||||
serde-inline-default = "0.2.1"
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
48
pumpkin-config/src/logging.rs
Normal file
48
pumpkin-config/src/logging.rs
Normal 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,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -2,6 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::vector3::Vector3;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
/// Aka Block Position
|
||||
pub struct WorldPosition(pub Vector3<i32>);
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)?
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
261
pumpkin-macros/src/block_state.rs
Normal file
261
pumpkin-macros/src/block_state.rs
Normal 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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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`
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
18
pumpkin-protocol/src/client/play/c_entity_status.rs
Normal file
18
pumpkin-protocol/src/client/play/c_entity_status.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
8
pumpkin-protocol/src/client/play/c_keep_alive.rs
Normal file
8
pumpkin-protocol/src/client/play/c_keep_alive.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use pumpkin_macros::packet;
|
||||
use serde::Serialize;
|
||||
|
||||
#[packet(0x26)]
|
||||
#[derive(Serialize)]
|
||||
pub struct CKeepAlive {
|
||||
pub keep_alive_id: i64,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
22
pumpkin-protocol/src/client/play/c_set_health.rs
Normal file
22
pumpkin-protocol/src/client/play/c_set_health.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()))?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
8
pumpkin-protocol/src/server/play/s_keep_alive.rs
Normal file
8
pumpkin-protocol/src/server/play/s_keep_alive.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use pumpkin_macros::packet;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[packet(0x18)]
|
||||
#[derive(Deserialize)]
|
||||
pub struct SKeepAlive {
|
||||
pub keep_alive_id: i64,
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
17
pumpkin-world/README.md
Normal 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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
73
pumpkin-world/src/block/block_state.rs
Normal file
73
pumpkin-world/src/block/block_state.rs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod generator;
|
||||
mod generic_generator;
|
||||
mod implementation;
|
||||
mod noise;
|
||||
mod seed;
|
||||
|
||||
pub use generator::WorldGenerator;
|
||||
|
||||
65
pumpkin-world/src/world_gen/noise/mod.rs
Normal file
65
pumpkin-world/src/world_gen/noise/mod.rs
Normal 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
|
||||
}
|
||||
1125
pumpkin-world/src/world_gen/noise/perlin.rs
Normal file
1125
pumpkin-world/src/world_gen/noise/perlin.rs
Normal file
File diff suppressed because it is too large
Load Diff
702
pumpkin-world/src/world_gen/noise/simplex.rs
Normal file
702
pumpkin-world/src/world_gen/noise/simplex.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
®istry.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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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))?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user